diff options
Diffstat (limited to 'third_party/heimdal/kdc')
66 files changed, 33950 insertions, 0 deletions
diff --git a/third_party/heimdal/kdc/Makefile.am b/third_party/heimdal/kdc/Makefile.am new file mode 100644 index 0000000..ca58359 --- /dev/null +++ b/third_party/heimdal/kdc/Makefile.am @@ -0,0 +1,263 @@ +# $Id$ + +include $(top_srcdir)/Makefile.am.common + +WFLAGS += $(WFLAGS_ENUM_CONV) + +AM_CPPFLAGS += $(INCLUDE_libintl) $(INCLUDE_openssl_crypto) -I$(srcdir)/../lib/krb5 + +lib_LTLIBRARIES = ipc_csr_authorizer.la \ + negotiate_token_validator.la \ + libkdc.la + +if HAVE_CJWT +lib_LTLIBRARIES += cjwt_token_validator.la +endif +if OPENLDAP +lib_LTLIBRARIES += altsecid_gss_preauth_authorizer.la +endif + + +bin_PROGRAMS = string2key + +sbin_PROGRAMS = kstash + +libexec_PROGRAMS = hprop hpropd kdc digest-service \ + test_token_validator test_csr_authorizer test_kdc_ca + +noinst_PROGRAMS = kdc-replay kdc-tester + +man_MANS = bx509d.8 httpkadmind.8 kdc.8 kstash.8 hprop.8 hpropd.8 string2key.8 + +hprop_SOURCES = hprop.c mit_dump.c hprop.h +hpropd_SOURCES = hpropd.c hprop.h + +kstash_SOURCES = kstash.c headers.h + +string2key_SOURCES = string2key.c headers.h + +if HAVE_MICROHTTPD +bx509d_SOURCES = bx509d.c +bx509d_AM_CPPFLAGS = $(AM_CPPFLAGS) $(MICROHTTPD_CFLAGS) +bx509d_LDADD = -ldl \ + $(top_builddir)/lib/hdb/libhdb.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 += 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 = \ + digest-service.c + +kdc_SOURCES = connect.c \ + config.c \ + announce.c \ + main.c + +kdc_tester_SOURCES = \ + config.c \ + kdc-tester.c + +test_token_validator_SOURCES = test_token_validator.c +test_csr_authorizer_SOURCES = test_csr_authorizer.c +test_kdc_ca_SOURCES = test_kdc_ca.c + +# Token plugins (for bx509d) +if HAVE_CJWT +cjwt_token_validator_la_SOURCES = cjwt_token_validator.c +cjwt_token_validator_la_CFLAGS = $(CJSON_CFLAGS) $(CJWT_CFLAGS) +cjwt_token_validator_la_LDFLAGS = -module $(CJSON_LIBS) $(CJWT_LIBS) +endif + +negotiate_token_validator_la_SOURCES = negotiate_token_validator.c +negotiate_token_validator_la_LDFLAGS = -module $(LIB_gssapi) +# CSR Authorizer plugins (for kdc/kx509 and bx509d) +ipc_csr_authorizer_la_SOURCES = ipc_csr_authorizer.c +ipc_csr_authorizer_la_LDFLAGS = -module \ + $(top_builddir)/lib/krb5/libkrb5.la \ + $(top_builddir)/lib/hx509/libhx509.la \ + $(top_builddir)/lib/ipc/libheim-ipcc.la \ + $(top_builddir)/lib/roken/libroken.la + +# GSS-API authorization plugins +if OPENLDAP +altsecid_gss_preauth_authorizer_la_SOURCES = altsecid_gss_preauth_authorizer.c +altsecid_gss_preauth_authorizer_la_LDFLAGS = -module \ + $(top_builddir)/lib/gssapi/libgssapi.la \ + $(top_builddir)/lib/krb5/libkrb5.la \ + $(LIB_openldap) +endif + +libkdc_la_CPPFLAGS = -DBUILD_KDC_LIB $(AM_CPPFLAGS) + +libkdc_la_SOURCES = \ + default_config.c \ + ca.c \ + set_dbinfo.c \ + digest.c \ + fast.c \ + kdc_locl.h \ + kerberos5.c \ + krb5tgs.c \ + pkinit.c \ + pkinit-ec.c \ + mssfu.c \ + log.c \ + misc.c \ + kx509.c \ + token_validator.c \ + csr_authorizer.c \ + process.c \ + kdc-plugin.c \ + gss_preauth.c + +KDC_PROTOS = $(srcdir)/kdc-protos.h $(srcdir)/kdc-private.h + +ALL_OBJECTS = $(kdc_OBJECTS) +ALL_OBJECTS += $(kdc_replay_OBJECTS) +ALL_OBJECTS += $(kdc_tester_OBJECTS) +ALL_OBJECTS += $(test_token_validator_OBJECTS) +ALL_OBJECTS += $(test_csr_authorizer_OBJECTS) +ALL_OBJECTS += $(test_kdc_ca_OBJECTS) +ALL_OBJECTS += $(libkdc_la_OBJECTS) +ALL_OBJECTS += $(string2key_OBJECTS) +ALL_OBJECTS += $(kstash_OBJECTS) +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 += $(test_token_validator_OBJECTS) +ALL_OBJECTS += $(test_csr_authorizer_OBJECTS) +ALL_OBJECTS += $(test_kdc_ca_OBJECTS) +ALL_OBJECTS += $(ipc_csr_authorizer_la_OBJECTS) +ALL_OBJECTS += $(negotiate_token_validator_la_OBJECTS) + +$(ALL_OBJECTS): $(KDC_PROTOS) + +libkdc_la_LDFLAGS = -version-info 2:0:0 + +if versionscript +libkdc_la_LDFLAGS += $(LDFLAGS_VERSION_SCRIPT)$(srcdir)/version-script.map +endif + +$(libkdc_la_OBJECTS): $(srcdir)/version-script.map + +$(srcdir)/kdc-protos.h: $(libkdc_la_SOURCES) + cd $(srcdir) && perl ../cf/make-proto.pl -E KDC_LIB -q -P comment -o kdc-protos.h $(libkdc_la_SOURCES) || rm -f kdc-protos.h + +$(srcdir)/kdc-private.h: $(libkdc_la_SOURCES) + cd $(srcdir) && perl ../cf/make-proto.pl -q -P comment -p kdc-private.h $(libkdc_la_SOURCES) || rm -f kdc-private.h + + +hprop_LDADD = \ + $(top_builddir)/lib/hdb/libhdb.la \ + $(top_builddir)/lib/krb5/libkrb5.la \ + $(LIB_kdb) \ + $(LIB_hcrypto) \ + $(top_builddir)/lib/asn1/libasn1.la \ + $(LIB_roken) \ + $(DB3LIB) $(DB1LIB) $(LMDBLIB) $(NDBMLIB) + +hpropd_LDADD = \ + $(top_builddir)/lib/hdb/libhdb.la \ + $(top_builddir)/lib/krb5/libkrb5.la \ + $(LIB_kdb) \ + $(LIB_hcrypto) \ + $(top_builddir)/lib/asn1/libasn1.la \ + $(LIB_roken) \ + $(DB3LIB) $(DB1LIB) $(LMDBLIB) $(NDBMLIB) + +if PKINIT +LIB_pkinit = $(top_builddir)/lib/hx509/libhx509.la +endif + +libkdc_la_LIBADD = \ + $(LIB_pkinit) \ + $(top_builddir)/lib/hdb/libhdb.la \ + $(top_builddir)/lib/krb5/libkrb5.la \ + $(top_builddir)/lib/gssapi/libgssapi.la \ + $(top_builddir)/lib/gss_preauth/libgss_preauth.la \ + $(LIB_kdb) \ + $(top_builddir)/lib/ntlm/libheimntlm.la \ + $(LIB_hcrypto) \ + $(LIB_openssl_crypto) \ + $(top_builddir)/lib/asn1/libasn1.la \ + $(LIB_roken) \ + $(DB3LIB) $(DB1LIB) $(LMDBLIB) $(NDBMLIB) + +LDADD = $(top_builddir)/lib/hdb/libhdb.la \ + $(top_builddir)/lib/krb5/libkrb5.la \ + $(LIB_hcrypto) \ + $(top_builddir)/lib/asn1/libasn1.la \ + $(LIB_roken) \ + $(DB3LIB) $(DB1LIB) $(LMDBLIB) $(NDBMLIB) + +kdc_LDADD = libkdc.la $(LDADD) $(LIB_pidfile) $(CAPNG_LIBS) + +if FRAMEWORK_SECURITY +kdc_LDFLAGS = -framework SystemConfiguration -framework CoreFoundation +endif +kdc_CFLAGS = $(CAPNG_CFLAGS) + +digest_service_LDADD = \ + libkdc.la \ + $(top_builddir)/lib/ntlm/libheimntlm.la \ + $(top_builddir)/lib/ipc/libheim-ipcs.la \ + $(LDADD) $(LIB_pidfile) +kdc_replay_LDADD = libkdc.la $(LDADD) $(LIB_pidfile) +kdc_tester_LDADD = libkdc.la $(LDADD) $(LIB_pidfile) $(LIB_heimbase) +test_token_validator_LDADD = libkdc.la $(LDADD) $(LIB_pidfile) $(LIB_heimbase) $(LIB_gssapi) +test_csr_authorizer_LDADD = libkdc.la \ + $(top_builddir)/lib/hx509/libhx509.la \ + $(LDADD) \ + $(LIB_pidfile) \ + $(LIB_heimbase) \ + $(top_builddir)/lib/ipc/libheim-ipcs.la +test_kdc_ca_LDADD = libkdc.la $(top_builddir)/lib/hx509/libhx509.la $(LDADD) $(LIB_pidfile) $(LIB_heimbase) + +include_HEADERS = kdc.h $(srcdir)/kdc-protos.h + +noinst_HEADERS = $(srcdir)/kdc-private.h + +krb5dir = $(includedir)/krb5 +krb5_HEADERS = kdc-audit.h kdc-plugin.h kdc-accessors.h token_validator_plugin.h csr_authorizer_plugin.h gss_preauth_authorizer_plugin.h + +build_HEADERZ = $(krb5_HEADERS) # XXX + +EXTRA_DIST = \ + hprop-version.rc \ + hpropd-version.rc \ + kdc-version.rc \ + kstash-version.rc \ + libkdc-version.rc \ + string2key-version.rc \ + libkdc-exports.def \ + NTMakefile $(man_MANS) version-script.map diff --git a/third_party/heimdal/kdc/NTMakefile b/third_party/heimdal/kdc/NTMakefile new file mode 100644 index 0000000..aca65b1 --- /dev/null +++ b/third_party/heimdal/kdc/NTMakefile @@ -0,0 +1,167 @@ +######################################################################## +# +# Copyright (c) 2009-2016, Secure Endpoints Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# - 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. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 +# COPYRIGHT HOLDER 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. +# + +RELDIR=kdc + +!include ../windows/NTMakefile.w32 + +intcflags=-I$(OBJ) -I$(SRC)\lib\gssapi -I$(OBJDIR)\lib\gssapi -I$(OBJDIR)\lib\gss_preauth -DBUILD_KDC_LIB + +BINPROGRAMS=$(BINDIR)\string2key.exe + +SBINPROGRAMS=$(SBINDIR)\kstash.exe + +LIBEXECPROGRAMS= \ + $(LIBEXECDIR)\hprop.exe \ + $(LIBEXECDIR)\hpropd.exe \ + $(LIBEXECDIR)\kdc.exe \ +# $(LIBEXECDIR)\digest-service.exe + +NOINST_PROGRAMS=$(OBJ)\kdc-replay.exe + +INCFILES=\ + $(INCDIR)\kdc.h \ + $(INCDIR)\kdc-protos.h \ + $(INCDIR)\kdc-private.h \ + $(INCDIR)\krb5\kdc-audit.h \ + $(INCDIR)\krb5\kdc-plugin.h \ + $(INCDIR)\krb5\kdc-accessors.h + +all:: $(INCFILES) $(LIBKDC) $(BINPROGRAMS) $(SBINPROGRAMS) $(LIBEXECPROGRAMS) + +clean:: + -$(RM) $(LIBKDC) + -$(RM) $(BINPROGRAMS:.exe=.*) $(SBINPROGRAMS:.exe=.*) $(LIBEXECPROGRAMS:.exe=.*) + +BIN_LIBS=\ + $(LIBHDB) \ + $(LIBHEIMDAL) \ + $(LIBROKEN) \ + $(LIBVERS) + +$(LIBEXECDIR)\hprop.exe: $(OBJ)\hprop.obj $(OBJ)\mit_dump.obj $(BIN_LIBS) $(OBJ)\hprop-version.res + $(EXECONLINK) + $(EXEPREP) + +$(LIBEXECDIR)\hpropd.exe: $(OBJ)\hpropd.obj $(BIN_LIBS) $(OBJ)\hpropd-version.res + $(EXECONLINK) + $(EXEPREP) + +$(SBINDIR)\kstash.exe: $(OBJ)\kstash.obj $(BIN_LIBS) $(OBJ)\kstash-version.res + $(EXECONLINK) + $(EXEPREP) + +$(BINDIR)\string2key.exe: $(OBJ)\string2key.obj $(BIN_LIBS) $(OBJ)\string2key-version.res + $(EXECONLINK) + $(EXEPREP) + +$(BINDIR)\digest-service.exe: $(OBJ)\digest-service.obj $(BIN_LIBS) + $(EXECONLINK) + $(EXEPREP) + +$(LIBEXECDIR)\kdc.exe: \ + $(OBJ)\connect.obj $(OBJ)\config.obj $(OBJ)\announce.obj \ + $(OBJ)\main.obj $(OBJ)\kdc-version.res \ + $(LIBKDC) $(BIN_LIBS) $(LIB_openssl_crypto) + $(EXECONLINK) + $(EXEPREP) + +LIBKDC_OBJS=\ + $(OBJ)\default_config.obj \ + $(OBJ)\ca.obj \ + $(OBJ)\kx509.obj \ + $(OBJ)\set_dbinfo.obj \ + $(OBJ)\digest.obj \ + $(OBJ)\fast.obj \ + $(OBJ)\kerberos5.obj \ + $(OBJ)\krb5tgs.obj \ + $(OBJ)\pkinit.obj \ + $(OBJ)\pkinit-ec.obj \ + $(OBJ)\mssfu.obj \ + $(OBJ)\log.obj \ + $(OBJ)\misc.obj \ + $(OBJ)\kx509.obj \ + $(OBJ)\token_validator.obj \ + $(OBJ)\csr_authorizer.obj \ + $(OBJ)\process.obj \ + $(OBJ)\kdc-plugin.obj \ + $(OBJ)\gss_preauth.obj + +LIBKDC_LIBS=\ + $(LIBHDB) \ + $(LIBGSS_PREAUTH) \ + $(LIBGSSAPI) \ + $(LIBHEIMBASE) \ + $(LIBHEIMDAL) \ + $(LIBHEIMNTLM) \ + $(LIB_openssl_crypto) \ + $(LIBROKEN) + +LIBKDCRES=$(OBJ)\libkdc-version.res + +$(LIBEXECDIR)\libkdc.dll: $(LIBKDC_OBJS) $(LIBKDC_LIBS) $(LIBKDCRES) + $(DLLGUILINK) -implib:$(LIBKDC) -def:libkdc-exports.def + $(DLLPREP_NODIST) + +$(LIBKDC): $(LIBEXECDIR)\libkdc.dll + +clean:: + -$(RM) $(LIBEXECDIR)\libkdc.* + +libkdc_la_SOURCES = \ + default_config.c \ + ca.c \ + set_dbinfo.c \ + digest.c \ + fast.c \ + kdc_locl.h \ + kerberos5.c \ + krb5tgs.c \ + pkinit.c \ + pkinit-ec.c \ + mssfu.c \ + log.c \ + misc.c \ + kx509.c \ + token_validator.c \ + csr_authorizer.c \ + process.c \ + kdc-plugin.c \ + gss_preauth.c + +$(OBJ)\kdc-protos.h: $(libkdc_la_SOURCES) + cd $(SRCDIR) + $(PERL) ..\cf\make-proto.pl -E KDC_LIB -q -P remove -o $@ $(libkdc_la_SOURCES) \ + || $(RM) $@ + +$(OBJ)\kdc-private.h: $(libkdc_la_SOURCES) + $(PERL) ..\cf\make-proto.pl -q -P remove -p $@ $(libkdc_la_SOURCES) \ + || $(RM) $@ diff --git a/third_party/heimdal/kdc/altsecid_gss_preauth_authorizer.c b/third_party/heimdal/kdc/altsecid_gss_preauth_authorizer.c new file mode 100644 index 0000000..d48ea58 --- /dev/null +++ b/third_party/heimdal/kdc/altsecid_gss_preauth_authorizer.c @@ -0,0 +1,541 @@ +/* + * Copyright (c) 2021, PADL Software Pty Ltd. + * All rights reserved. + * + * Portions Copyright (c) 2004 Kungliga Tekniska Högskolan + * + * 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 PADL Software 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 PADL SOFTWARE 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 PADL SOFTWARE 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. + */ + +/* + * This plugin authorizes federated GSS-API pre-authentication clients by + * querying an AD DC in the KDC realm for the altSecurityIdentities + * attribute. + * + * For example, GSS-API initiator foo@AAA.H5L.SE using the eap-aes128 + * mechanism to authenticate in realm H5L.SE would require a user entry + * where altSecurityIdentities equals either: + * + * EAP:foo@AAA.H5L.SE + * EAP-AES128:foo@AAA.H5L.SE + * + * (Stripping the mechanism name after the hyphen is a convention + * intended to allow mechanism variants to be grouped together.) + * + * Once the user entry is found, the name is canonicalized by reading the + * sAMAccountName attribute and concatenating it with the KDC realm, + * specifically the canonicalized realm of the WELLKNOWN/FEDERATED HDB + * entry. + * + * The KDC will need to have access to a default credentials cache, or + * OpenLDAP will need to be linked against a version of Cyrus SASL and + * Heimdal that supports keytab credentials. + */ + +#include "kdc_locl.h" + +#include <resolve.h> +#include <common_plugin.h> +#include <heimqueue.h> + +#include <gssapi/gssapi.h> +#include <gssapi_mech.h> + +#include <ldap.h> + +#include "gss_preauth_authorizer_plugin.h" + +#ifndef PAC_REQUESTOR_SID +#define PAC_REQUESTOR_SID 18 +#endif + +struct ad_server_tuple { + HEIM_TAILQ_ENTRY(ad_server_tuple) link; + char *realm; + LDAP *ld; +#ifdef LDAP_OPT_X_SASL_GSS_CREDS + gss_cred_id_t gss_cred; +#endif +}; + +struct altsecid_gss_preauth_authorizer_context { + HEIM_TAILQ_HEAD(ad_server_tuple_list, ad_server_tuple) servers; +}; + +static int +sasl_interact(LDAP *ld, unsigned flags, void *defaults, void *interact) +{ + return LDAP_SUCCESS; +} + +#ifdef LDAP_OPT_X_SASL_GSS_CREDS +static krb5_error_code +ad_acquire_cred(krb5_context context, + krb5_const_realm realm, + struct ad_server_tuple *server) +{ + const char *keytab_name = NULL; + char *keytab_name_buf = NULL; + krb5_error_code ret; + + OM_uint32 major, minor; + gss_key_value_element_desc client_keytab; + gss_key_value_set_desc cred_store; + gss_OID_set_desc desired_mechs; + + desired_mechs.count = 1; + desired_mechs.elements = GSS_KRB5_MECHANISM; + + keytab_name = krb5_config_get_string(context, NULL, "kdc", realm, + "gss_altsecid_authorizer_keytab_name", NULL); + if (keytab_name == NULL) + keytab_name = krb5_config_get_string(context, NULL, "kdc", + "gss_altsecid_authorizer_keytab_name", NULL); + if (keytab_name == NULL) { + ret = _krb5_kt_client_default_name(context, &keytab_name_buf); + if (ret) + return ret; + + keytab_name = keytab_name_buf; + } + + client_keytab.key = "client_keytab"; + client_keytab.value = keytab_name; + + cred_store.count = 1; + cred_store.elements = &client_keytab; + + major = gss_acquire_cred_from(&minor, GSS_C_NO_NAME, GSS_C_INDEFINITE, + &desired_mechs, GSS_C_INITIATE, + &cred_store, &server->gss_cred, NULL, NULL); + if (GSS_ERROR(major)) + ret = minor ? minor : KRB5_KT_NOTFOUND; + else + ret = 0; + + krb5_xfree(keytab_name_buf); + + return ret; +} +#endif + +static krb5_boolean +is_recoverable_ldap_err_p(int lret) +{ + return + (lret == LDAP_SERVER_DOWN || + lret == LDAP_TIMEOUT || + lret == LDAP_UNAVAILABLE || + lret == LDAP_BUSY || + lret == LDAP_CONNECT_ERROR); +} + +static krb5_error_code +ad_connect(krb5_context context, + krb5_const_realm realm, + struct ad_server_tuple *server) +{ + krb5_error_code ret; + struct { + char *server; + int port; + } *s, *servers = NULL; + size_t i, num_servers = 0; + + { + struct rk_dns_reply *r; + struct rk_resource_record *rr; + char *domain; + + asprintf(&domain, "_ldap._tcp.%s", realm); + if (domain == NULL) { + ret = krb5_enomem(context); + goto out; + } + + r = rk_dns_lookup(domain, "SRV"); + free(domain); + if (r == NULL) { + krb5_set_error_message(context, KRB5KDC_ERR_SVC_UNAVAILABLE, + "Couldn't find AD DC in DNS"); + ret = KRB5KDC_ERR_SVC_UNAVAILABLE; + goto out; + } + + for (rr = r->head ; rr != NULL; rr = rr->next) { + if (rr->type != rk_ns_t_srv) + continue; + s = realloc(servers, sizeof(*servers) * (num_servers + 1)); + if (s == NULL) { + ret = krb5_enomem(context); + rk_dns_free_data(r); + goto out; + } + servers = s; + num_servers++; + servers[num_servers - 1].port = rr->u.srv->port; + servers[num_servers - 1].server = strdup(rr->u.srv->target); + } + rk_dns_free_data(r); + } + +#ifdef LDAP_OPT_X_SASL_GSS_CREDS + if (server->gss_cred == GSS_C_NO_CREDENTIAL) { + ret = ad_acquire_cred(context, realm, server); + if (ret) + goto out; + } +#endif + + for (i = 0; i < num_servers; i++) { + int lret, version = LDAP_VERSION3; + LDAP *ld; + char *url = NULL; + + asprintf(&url, "ldap://%s:%d", servers[i].server, servers[i].port); + if (url == NULL) { + ret = krb5_enomem(context); + goto out; + } + + lret = ldap_initialize(&ld, url); + free(url); + if (lret != LDAP_SUCCESS) + continue; + + ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version); + ldap_set_option(ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); +#ifdef LDAP_OPT_X_SASL_GSS_CREDS + ldap_set_option(ld, LDAP_OPT_X_SASL_GSS_CREDS, server->gss_cred); +#endif + + lret = ldap_sasl_interactive_bind_s(ld, NULL, "GSS-SPNEGO", + NULL, NULL, LDAP_SASL_QUIET, + sasl_interact, NULL); + if (lret != LDAP_SUCCESS) { + krb5_set_error_message(context, 0, + "Couldn't bind to AD DC %s:%d: %s", + servers[i].server, servers[i].port, + ldap_err2string(lret)); + ldap_unbind_ext_s(ld, NULL, NULL); + continue; + } + + server->ld = ld; + break; + } + + ret = (server->ld != NULL) ? 0 : KRB5_KDC_UNREACH; + +out: + for (i = 0; i < num_servers; i++) + free(servers[i].server); + free(servers); + + if (ret && server->ld) { + ldap_unbind_ext_s(server->ld, NULL, NULL); + server->ld = NULL; + } + + return ret; +} + +static krb5_error_code +ad_lookup(krb5_context context, + krb5_const_realm realm, + struct ad_server_tuple *server, + gss_const_name_t initiator_name, + gss_const_OID mech_type, + krb5_principal *canon_principal, + kdc_data_t *requestor_sid) +{ + krb5_error_code ret; + OM_uint32 minor; + const char *mech_type_str, *p; + char *filter = NULL; + gss_buffer_desc initiator_name_buf = GSS_C_EMPTY_BUFFER; + LDAPMessage *m = NULL, *m0; + char *basedn = NULL; + int lret; + char *attrs[] = { "sAMAccountName", "objectSid", NULL }; + struct berval **values = NULL; + + *canon_principal = NULL; + if (requestor_sid) + *requestor_sid = NULL; + + mech_type_str = gss_oid_to_name(mech_type); + if (mech_type_str == NULL) { + ret = KRB5_PREAUTH_BAD_TYPE; /* should never happen */ + goto out; + } + + ret = KRB5_KDC_ERR_CLIENT_NOT_TRUSTED; + + if (GSS_ERROR(gss_display_name(&minor, initiator_name, + &initiator_name_buf, NULL))) + goto out; + + if ((p = strrchr(mech_type_str, '-')) != NULL) { + asprintf(&filter, "(&(objectClass=user)" + "(|(altSecurityIdentities=%.*s:%.*s)(altSecurityIdentities=%s:%.*s)))", + (int)(p - mech_type_str), mech_type_str, + (int)initiator_name_buf.length, (char *)initiator_name_buf.value, + mech_type_str, + (int)initiator_name_buf.length, + (char *)initiator_name_buf.value); + } else { + asprintf(&filter, "(&(objectClass=user)(altSecurityIdentities=%s:%.*s))", + mech_type_str, + (int)initiator_name_buf.length, + (char *)initiator_name_buf.value); + } + if (filter == NULL) + goto enomem; + + lret = ldap_domain2dn(realm, &basedn); + if (lret != LDAP_SUCCESS) + goto out; + + lret = ldap_search_ext_s(server->ld, basedn, LDAP_SCOPE_SUBTREE, + filter, attrs, 0, + NULL, NULL, NULL, 1, &m); + if (lret == LDAP_SIZELIMIT_EXCEEDED) + ret = KRB5KDC_ERR_PRINCIPAL_NOT_UNIQUE; + else if (is_recoverable_ldap_err_p(lret)) + ret = KRB5KDC_ERR_SVC_UNAVAILABLE; + if (lret != LDAP_SUCCESS) + goto out; + + m0 = ldap_first_entry(server->ld, m); + if (m0 == NULL) + goto out; + + values = ldap_get_values_len(server->ld, m0, "sAMAccountName"); + if (values == NULL || + ldap_count_values_len(values) == 0) + goto out; + + ret = krb5_make_principal(context, canon_principal, realm, + values[0]->bv_val, NULL); + if (ret) + goto out; + + if (requestor_sid) { + ldap_value_free_len(values); + + values = ldap_get_values_len(server->ld, m0, "objectSid"); + if (values == NULL || + ldap_count_values_len(values) == 0) + goto out; + + *requestor_sid = kdc_data_create(values[0]->bv_val, values[0]->bv_len); + if (*requestor_sid == NULL) + goto enomem; + } + + goto out; + +enomem: + ret = krb5_enomem(context); + goto out; + +out: + if (ret) { + krb5_free_principal(context, *canon_principal); + *canon_principal = NULL; + + if (requestor_sid) { + kdc_object_release(*requestor_sid); + *requestor_sid = NULL; + } + } + + ldap_value_free_len(values); + ldap_msgfree(m); + ldap_memfree(basedn); + free(filter); + gss_release_buffer(&minor, &initiator_name_buf); + + return ret; +} + +static KRB5_LIB_CALL krb5_error_code +authorize(void *ctx, + astgs_request_t r, + gss_const_name_t initiator_name, + gss_const_OID mech_type, + OM_uint32 ret_flags, + krb5_boolean *authorized, + krb5_principal *mapped_name) +{ + struct altsecid_gss_preauth_authorizer_context *c = ctx; + struct ad_server_tuple *server = NULL; + krb5_error_code ret; + krb5_context context = kdc_request_get_context((kdc_request_t)r); + const hdb_entry *client = kdc_request_get_client(r); + krb5_const_principal server_princ = kdc_request_get_server_princ(r); + krb5_const_realm realm = krb5_principal_get_realm(context, client->principal); + krb5_boolean reconnect_p = FALSE; + krb5_boolean is_tgs; + kdc_data_t requestor_sid = NULL; + + *authorized = FALSE; + *mapped_name = NULL; + + if (!krb5_principal_is_federated(context, client->principal) || + (ret_flags & GSS_C_ANON_FLAG)) + return KRB5_PLUGIN_NO_HANDLE; + + is_tgs = krb5_principal_is_krbtgt(context, server_princ); + + HEIM_TAILQ_FOREACH(server, &c->servers, link) { + if (strcmp(realm, server->realm) == 0) + break; + } + + if (server == NULL) { + server = calloc(1, sizeof(*server)); + if (server == NULL) + return krb5_enomem(context); + + server->realm = strdup(realm); + if (server->realm == NULL) { + free(server); + return krb5_enomem(context); + } + + HEIM_TAILQ_INSERT_HEAD(&c->servers, server, link); + } + + do { + if (server->ld == NULL) { + ret = ad_connect(context, realm, server); + if (ret) + return ret; + } + + ret = ad_lookup(context, realm, server, + initiator_name, mech_type, + mapped_name, is_tgs ? &requestor_sid : NULL); + if (ret == KRB5KDC_ERR_SVC_UNAVAILABLE) { + ldap_unbind_ext_s(server->ld, NULL, NULL); + server->ld = NULL; + + /* try to reconnect iff we haven't already tried */ + reconnect_p = !reconnect_p; + } + + *authorized = (ret == 0); + } while (reconnect_p); + + if (requestor_sid) { + kdc_request_set_attribute((kdc_request_t)r, + HSTR("org.h5l.gss-pa-requestor-sid"), requestor_sid); + kdc_object_release(requestor_sid); + } + + return ret; +} + +static KRB5_LIB_CALL krb5_error_code +finalize_pac(void *ctx, astgs_request_t r) +{ + kdc_data_t requestor_sid; + + requestor_sid = kdc_request_get_attribute((kdc_request_t)r, + HSTR("org.h5l.gss-pa-requestor-sid")); + if (requestor_sid == NULL) + return 0; + + kdc_audit_setkv_object((kdc_request_t)r, "gss_requestor_sid", requestor_sid); + + return kdc_request_add_pac_buffer(r, PAC_REQUESTOR_SID, + kdc_data_get_data(requestor_sid)); +} + +static KRB5_LIB_CALL krb5_error_code +init(krb5_context context, void **contextp) +{ + struct altsecid_gss_preauth_authorizer_context *c; + + c = calloc(1, sizeof(*c)); + if (c == NULL) + return krb5_enomem(context); + + HEIM_TAILQ_INIT(&c->servers); + + *contextp = c; + return 0; +} + +static KRB5_LIB_CALL void +fini(void *context) +{ + struct altsecid_gss_preauth_authorizer_context *c = context; + struct ad_server_tuple *server, *next; + OM_uint32 minor; + + HEIM_TAILQ_FOREACH_SAFE(server, &c->servers, link, next) { + free(server->realm); + ldap_unbind_ext_s(server->ld, NULL, NULL); +#ifdef LDAP_OPT_X_SASL_GSS_CREDS + gss_release_cred(&minor, &server->gss_cred); +#endif + free(server); + } +} + +static krb5plugin_gss_preauth_authorizer_ftable plug_desc = + { 1, init, fini, authorize, finalize_pac }; + +static krb5plugin_gss_preauth_authorizer_ftable *plugs[] = { &plug_desc }; + +static uintptr_t +altsecid_gss_preauth_authorizer_get_instance(const char *libname) +{ + if (strcmp(libname, "krb5") == 0) + return krb5_get_instance(libname); + if (strcmp(libname, "kdc") == 0) + return kdc_get_instance(libname); + return 0; +} + +krb5_plugin_load_ft kdc_gss_preauth_authorizer_plugin_load; + +krb5_error_code KRB5_CALLCONV +kdc_gss_preauth_authorizer_plugin_load(heim_pcontext context, + krb5_get_instance_func_t *get_instance, + size_t *num_plugins, + krb5_plugin_common_ftable_cp **plugins) +{ + *get_instance = altsecid_gss_preauth_authorizer_get_instance; + *num_plugins = sizeof(plugs) / sizeof(plugs[0]); + *plugins = (krb5_plugin_common_ftable_cp *)plugs; + return 0; +} diff --git a/third_party/heimdal/kdc/announce.c b/third_party/heimdal/kdc/announce.c new file mode 100644 index 0000000..cf3fdc3 --- /dev/null +++ b/third_party/heimdal/kdc/announce.c @@ -0,0 +1,544 @@ +/* + * Copyright (c) 2008 Apple Inc. All Rights Reserved. + * + * Export of this software from the United States of America may require + * a specific license from the United States Government. It is the + * responsibility of any person or organization contemplating export to + * obtain such a license before exporting. + * + * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and + * distribute this software and its documentation for any purpose and + * without fee is hereby granted, provided that the above copyright + * notice appear in all copies and that both that copyright notice and + * this permission notice appear in supporting documentation, and that + * the name of Apple Inc. not be used in advertising or publicity pertaining + * to distribution of the software without specific, written prior + * permission. Apple Inc. makes no representations about the suitability of + * this software for any purpose. It is provided "as is" without express + * or implied warranty. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + */ + +#include "kdc_locl.h" + +#if defined(__APPLE__) && defined(HAVE_GCD) + +#include <CoreFoundation/CoreFoundation.h> +#include <SystemConfiguration/SCDynamicStore.h> +#include <SystemConfiguration/SCDynamicStoreCopySpecific.h> +#include <SystemConfiguration/SCDynamicStoreKey.h> + +#include <dispatch/dispatch.h> + +#include <asl.h> +#include <resolv.h> + +#include <dns_sd.h> +#include <err.h> + +static krb5_kdc_configuration *announce_config; +static krb5_context announce_context; + +struct entry { + DNSRecordRef recordRef; + char *domain; + char *realm; +#define F_EXISTS 1 +#define F_PUSH 2 + int flags; + struct entry *next; +}; + +/* #define REGISTER_SRV_RR */ + +static struct entry *g_entries = NULL; +static CFStringRef g_hostname = NULL; +static DNSServiceRef g_dnsRef = NULL; +static SCDynamicStoreRef g_store = NULL; +static dispatch_queue_t g_queue = NULL; + +#define LOG(...) asl_log(NULL, NULL, ASL_LEVEL_INFO, __VA_ARGS__) + +static void create_dns_sd(void); +static void destroy_dns_sd(void); +static void update_all(SCDynamicStoreRef, CFArrayRef, void *); + + +/* parameters */ +static CFStringRef NetworkChangedKey_BackToMyMac = CFSTR("Setup:/Network/BackToMyMac"); + + +static char * +CFString2utf8(CFStringRef string) +{ + size_t size; + char *str; + + size = 1 + CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8); + str = malloc(size); + if (str == NULL) + return NULL; + + if (CFStringGetCString(string, str, size, kCFStringEncodingUTF8) == false) { + free(str); + return NULL; + } + return str; +} + +/* + * + */ + +static void +retry_timer(void) +{ + dispatch_source_t s; + dispatch_time_t t; + + s = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, + 0, 0, g_queue); + t = dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC); + dispatch_source_set_timer(s, t, 0, NSEC_PER_SEC); + dispatch_source_set_event_handler(s, ^{ + create_dns_sd(); + dispatch_release(s); + }); + dispatch_resume(s); +} + +/* + * + */ + +static void +create_dns_sd(void) +{ + DNSServiceErrorType error; + dispatch_source_t s; + + error = DNSServiceCreateConnection(&g_dnsRef); + if (error) { + retry_timer(); + return; + } + + dispatch_suspend(g_queue); + + s = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + DNSServiceRefSockFD(g_dnsRef), + 0, g_queue); + + dispatch_source_set_event_handler(s, ^{ + DNSServiceErrorType ret = DNSServiceProcessResult(g_dnsRef); + /* on error tear down and set timer to recreate */ + if (ret != kDNSServiceErr_NoError && ret != kDNSServiceErr_Transient) { + dispatch_source_cancel(s); + } + }); + + dispatch_source_set_cancel_handler(s, ^{ + destroy_dns_sd(); + retry_timer(); + dispatch_release(s); + }); + + dispatch_resume(s); + + /* Do the first update ourself */ + update_all(g_store, NULL, NULL); + dispatch_resume(g_queue); +} + +static void +domain_add(const char *domain, const char *realm, int flag) +{ + struct entry *e; + + for (e = g_entries; e != NULL; e = e->next) { + if (strcmp(domain, e->domain) == 0 && strcmp(realm, e->realm) == 0) { + e->flags |= flag; + return; + } + } + + LOG("Adding realm %s to domain %s", realm, domain); + + e = calloc(1, sizeof(*e)); + if (e == NULL) + return; + e->domain = strdup(domain); + e->realm = strdup(realm); + if (e->domain == NULL || e->realm == NULL) { + free(e->domain); + free(e->realm); + free(e); + return; + } + e->flags = flag | F_PUSH; /* if we allocate, we push */ + e->next = g_entries; + g_entries = e; +} + +struct addctx { + int flags; + const char *realm; +}; + +static void +domains_add(const void *key, const void *value, void *context) +{ + char *str = CFString2utf8((CFStringRef)value); + struct addctx *ctx = context; + + if (str == NULL) + return; + if (str[0] != '\0') + domain_add(str, ctx->realm, F_EXISTS | ctx->flags); + free(str); +} + + +static void +dnsCallback(DNSServiceRef sdRef __attribute__((unused)), + DNSRecordRef RecordRef __attribute__((unused)), + DNSServiceFlags flags __attribute__((unused)), + DNSServiceErrorType errorCode __attribute__((unused)), + void *context __attribute__((unused))) +{ +} + +#ifdef REGISTER_SRV_RR + +/* + * Register DNS SRV rr for the realm. + */ + +static const char *register_names[2] = { + "_kerberos._tcp", + "_kerberos._udp" +}; + +static struct { + DNSRecordRef *val; + size_t len; +} srvRefs = { NULL, 0 }; + +static void +register_srv(const char *realm, const char *hostname, int port) +{ + unsigned char target[1024]; + int i; + int size; + + /* skip registering LKDC realms */ + if (strncmp(realm, "LKDC:", 5) == 0) + return; + + /* encode SRV-RR */ + target[0] = 0; /* priority */ + target[1] = 0; /* priority */ + target[2] = 0; /* weight */ + target[3] = 0; /* weigth */ + target[4] = (port >> 8) & 0xff; /* port */ + target[5] = (port >> 0) & 0xff; /* port */ + + size = dn_comp(hostname, target + 6, sizeof(target) - 6, NULL, NULL); + if (size < 0) + return; + + size += 6; + + LOG("register SRV rr for realm %s hostname %s:%d", realm, hostname, port); + + for (i = 0; i < sizeof(register_names)/sizeof(register_names[0]); i++) { + char name[kDNSServiceMaxDomainName]; + DNSServiceErrorType error; + void *ptr; + + ptr = realloc(srvRefs.val, sizeof(srvRefs.val[0]) * (srvRefs.len + 1)); + if (ptr == NULL) + errx(1, "malloc: out of memory"); + srvRefs.val = ptr; + + DNSServiceConstructFullName(name, NULL, register_names[i], realm); + + error = DNSServiceRegisterRecord(g_dnsRef, + &srvRefs.val[srvRefs.len], + kDNSServiceFlagsUnique | kDNSServiceFlagsShareConnection, + 0, + name, + kDNSServiceType_SRV, + kDNSServiceClass_IN, + size, + target, + 0, + dnsCallback, + NULL); + if (error) { + LOG("Failed to register SRV rr for realm %s: %d", realm, error); + } else + srvRefs.len++; + } +} + +static void +unregister_srv_realms(void) +{ + if (g_dnsRef) { + for (i = 0; i < srvRefs.len; i++) + DNSServiceRemoveRecord(g_dnsRef, srvRefs.val[i], 0); + } + free(srvRefs.val); + srvRefs.len = 0; + srvRefs.val = NULL; +} + +static void +register_srv_realms(CFStringRef host) +{ + krb5_error_code ret; + char *hostname; + size_t i; + + /* first unregister old names */ + + hostname = CFString2utf8(host); + if (hostname == NULL) + return; + + for(i = 0; i < announce_config->num_db; i++) { + char **realms, **r; + + if (announce_config->db[i]->hdb_get_realms == NULL) + continue; + + ret = (announce_config->db[i]->hdb_get_realms)(announce_context, &realms); + if (ret == 0) { + for (r = realms; r && *r; r++) + register_srv(*r, hostname, 88); + krb5_free_host_realm(announce_context, realms); + } + } + + free(hostname); +} +#endif /* REGISTER_SRV_RR */ + +static void +update_dns(void) +{ + DNSServiceErrorType error; + struct entry **e = &g_entries; + char *hostname; + + hostname = CFString2utf8(g_hostname); + if (hostname == NULL) + return; + + while (*e != NULL) { + /* remove if this wasn't updated */ + if (((*e)->flags & F_EXISTS) == 0) { + struct entry *drop = *e; + *e = (*e)->next; + + LOG("Deleting realm %s from domain %s", + drop->realm, drop->domain); + + if (drop->recordRef && g_dnsRef) + DNSServiceRemoveRecord(g_dnsRef, drop->recordRef, 0); + free(drop->domain); + free(drop->realm); + free(drop); + continue; + } + if ((*e)->flags & F_PUSH) { + struct entry *update = *e; + char *dnsdata, *name; + size_t len; + + len = strlen(update->realm); + asprintf(&dnsdata, "%c%s", (int)len, update->realm); + if (dnsdata == NULL) + errx(1, "malloc"); + + asprintf(&name, "_kerberos.%s.%s", hostname, update->domain); + if (name == NULL) + errx(1, "malloc"); + + if (update->recordRef) + DNSServiceRemoveRecord(g_dnsRef, update->recordRef, 0); + + error = DNSServiceRegisterRecord(g_dnsRef, + &update->recordRef, + kDNSServiceFlagsShared | kDNSServiceFlagsAllowRemoteQuery, + 0, + name, + kDNSServiceType_TXT, + kDNSServiceClass_IN, + len+1, + dnsdata, + 0, + dnsCallback, + NULL); + free(name); + free(dnsdata); + if (error) + errx(1, "failure to update entry for %s/%s", + update->domain, update->realm); + } + e = &(*e)->next; + } + free(hostname); +} + +static void +update_entries(SCDynamicStoreRef store, const char *realm, int flags) +{ + CFDictionaryRef btmm; + + /* we always announce in the local domain */ + domain_add("local", realm, F_EXISTS | flags); + + /* announce btmm */ + btmm = SCDynamicStoreCopyValue(store, NetworkChangedKey_BackToMyMac); + if (btmm) { + struct addctx addctx; + + addctx.flags = flags; + addctx.realm = realm; + + CFDictionaryApplyFunction(btmm, domains_add, &addctx); + CFRelease(btmm); + } +} + +static void +update_all(SCDynamicStoreRef store, CFArrayRef changedKeys, void *info) +{ + struct entry *e; + CFStringRef host; + int i, flags = 0; + + LOG("something changed, running update"); + + host = SCDynamicStoreCopyLocalHostName(store); + if (host == NULL) + return; + + if (g_hostname == NULL || CFStringCompare(host, g_hostname, 0) != kCFCompareEqualTo) { + if (g_hostname) + CFRelease(g_hostname); + g_hostname = CFRetain(host); + flags = F_PUSH; /* if hostname has changed, force push */ + +#ifdef REGISTER_SRV_RR + register_srv_realms(g_hostname); +#endif + } + + for (e = g_entries; e != NULL; e = e->next) + e->flags &= ~(F_EXISTS|F_PUSH); + + for(i = 0; i < announce_config->num_db; i++) { + krb5_error_code ret; + char **realms, **r; + + if (announce_config->db[i]->hdb_get_realms == NULL) + continue; + + ret = (announce_config->db[i]->hdb_get_realms)(announce_context, announce_config->db[i], &realms); + if (ret == 0) { + for (r = realms; r && *r; r++) + update_entries(store, *r, flags); + krb5_free_host_realm(announce_context, realms); + } + } + + update_dns(); + + CFRelease(host); +} + +static void +delete_all(void) +{ + struct entry *e; + + for (e = g_entries; e != NULL; e = e->next) + e->flags &= ~(F_EXISTS|F_PUSH); + + update_dns(); + if (g_entries != NULL) + errx(1, "Failed to remove all bonjour entries"); +} + +static void +destroy_dns_sd(void) +{ + if (g_dnsRef == NULL) + return; + + delete_all(); +#ifdef REGISTER_SRV_RR + unregister_srv_realms(); +#endif + + DNSServiceRefDeallocate(g_dnsRef); + g_dnsRef = NULL; +} + + +static SCDynamicStoreRef +register_notification(void) +{ + SCDynamicStoreRef store; + CFStringRef computerNameKey; + CFMutableArrayRef keys; + + computerNameKey = SCDynamicStoreKeyCreateHostNames(kCFAllocatorDefault); + + store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("Network watcher"), + update_all, NULL); + if (store == NULL) + errx(1, "SCDynamicStoreCreate"); + + keys = CFArrayCreateMutable(kCFAllocatorDefault, 2, &kCFTypeArrayCallBacks); + if (keys == NULL) + errx(1, "CFArrayCreateMutable"); + + CFArrayAppendValue(keys, computerNameKey); + CFArrayAppendValue(keys, NetworkChangedKey_BackToMyMac); + + if (SCDynamicStoreSetNotificationKeys(store, keys, NULL) == false) + errx(1, "SCDynamicStoreSetNotificationKeys"); + + CFRelease(computerNameKey); + CFRelease(keys); + + if (!SCDynamicStoreSetDispatchQueue(store, g_queue)) + errx(1, "SCDynamicStoreSetDispatchQueue"); + + return store; +} +#endif + +void +bonjour_announce(krb5_context context, krb5_kdc_configuration *config) +{ +#if defined(__APPLE__) && defined(HAVE_GCD) + g_queue = dispatch_queue_create("com.apple.kdc_announce", NULL); + if (!g_queue) + errx(1, "dispatch_queue_create"); + + g_store = register_notification(); + announce_config = config; + announce_context = context; + + create_dns_sd(); +#endif +} diff --git a/third_party/heimdal/kdc/bx509d.8 b/third_party/heimdal/kdc/bx509d.8 new file mode 100644 index 0000000..f940155 --- /dev/null +++ b/third_party/heimdal/kdc/bx509d.8 @@ -0,0 +1,480 @@ +.\" 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 BX509 8 +.Os HEIMDAL +.Sh NAME +.Nm bx509d +.Nd Authentication Bridge for Bearer tokens, Kerberos, and PKIX +.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 allow-GET +.Op Fl Fl no-allow-GET +.Op Fl Fl csrf-protection-type= Ns Ar CSRF-PROTECTION-TYPE +.Op Fl Fl csrf-header= Ns Ar HEADER-NAME +.Op Fl Fl csrf-key-file= Ns Ar FILE +.Op Fl Fl reverse-proxied +.Op Fl p Ar port number (default: 443) +.Op Fl Fl cache-dir= Ns Ar DIRECTORY +.Op Fl Fl cert= Ns Ar HX509-STORE +.Op Fl Fl private-key= Ns Ar HX509-STORE +.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 RESTful (HTTPS) GETs of +.Ar /get-cert , +.Ar /get-tgt , +.Ar /get-tgts , +and +.Ar /get-negotiate-token , +end-points that implement various experimental Heimdal features: +.Bl -bullet -compact -offset indent +.It +.Li an online CA service over HTTPS, +.It +.Li a kinit-over-HTTPS service, and +.It +.Li a Negotiate token over HTTPS service. +.El +.Pp +As well, a +.Ar /health +service can be used for checking service status. +.Pp +Supported options: +.Bl -tag -width Ds +.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 Fl allow-GET +.Xc +If given, then HTTP GET will be allowed for the various +end-points other than +.Ar /health . +Otherwise only HEAD and POST will be allowed. +By default GETs are allowed, but this will change soon. +.It Xo +.Fl Fl no-allow-GET +.Xc +If given then HTTP GETs will be rejected for the various +end-points other than +.Ar /health . +.It Xo +.Fl Fl csrf-protection-type= Ns Ar CSRF-PROTECTION-TYPE +.Xc +Possible values of +.Ar CSRF-PROTECTION-TYPE +are +.Bl -bullet -compact -offset indent +.It +.Li GET-with-header +.It +.Li GET-with-token +.It +.Li POST-with-header +.It +.Li POST-with-token +.El +This may be given multiple times. +The default is to require CSRF tokens for POST requests, and to +require neither a non-simple header nor a CSRF token for GET +requests. +.Pp +See +.Sx CROSS-SITE REQUEST FORGERY PROTECTION . +.It Xo +.Fl Fl csrf-header= Ns Ar HEADER-NAME +.Xc +If given, then all requests other than to the +.Ar /health +service must have the given request +.Ar HEADER-NAME +set (the value is irrelevant). +The +.Ar HEADER-NAME +must be a request header name such that a request having it makes +it not a +.Dq simple +request (see the Cross-Origin Resource Sharing specification). +Defaults to +.Ar X-CSRF . +.It Xo +.Fl Fl csrf-key-file= Ns Ar FILE +.Xc +If given, this file must contain a 16 byte binary key for keying +the HMAC used in CSRF token construction. +.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 cache-dir= Ns Ar DIRECTORY +.Xc +Directory for various caches. +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 , +.Fl Fl thread-per-client +.Xc +Uses a thread per-client instead of as many threads as there are +CPUs. +.It Xo +.Fl v , +.Fl Fl verbose= Ns Ar run verbosely +.Xc +verbose +.El +.Sh HTTP APIS +All HTTP APIs served by this program accept POSTs, with all +request parameters given as URI query parameters and/or as +form data in the POST request body, in either +.Ar application/x-www-form-urlencoded +or +.Ar multipart/formdata . +If request parameters are given both as URI query parameters +and as POST forms, then they are merged into a set. +.Pp +If GETs are enabled, then request parameters must be supplied +only as URI query parameters, as GET requests do not have request +bodies. +.Pp +URI query parameters must be of the form +.Ar param0=value¶m1=value... +.Pp +Some request parameters can only have one value. +If multiple values are given for such parameters, then either an +error will be produced, or only the first URI query parameter +value will be used, or the first POST form data parameter will be +used. +Other request parameters can have multiple values. +See below. +.Sh CROSS-SITE REQUEST FORGERY PROTECTION +.Em None +of the resources service by this service are intended to be +executed by web pages. +.Pp +All the resources provided by this service are +.Dq safe +in the sense that they do not change server-side state besides +logging, and in that they are idempotent, but they are +only safe to execute +.Em if and only if +the requesting party is trusted to see the response. +Since none of these resources are intended to be used from web +pages, it is important that web pages not be able to execute them +.Em and +observe the responses. +.Pp +In a web browser context, pages from other origins will be able +to attempt requests to this service, but should never be able to +see the responses because browsers normally wouldn't allow that. +Nonetheless, anti cross site request forgery (CSRF) protection +may be desirable. +.Pp +This service provides the following CSRF protection features: +.Bl -tag -width Ds -offset indent +.It requests are rejected if they have a +.Dq Referer +(except the experimental /get-negotiate-token end-point) +.It the service can be configured to require a header that would make the +request not Dq simple +.It GETs can be disabled (see options), thus requiring POSTs +.It GETs can be required to have a CSRF token (see below) +.It POSTs can be required to have a CSRF token +.El +.Pp +The experimental +.Ar /get-negotiate-token +end-point, however, always accepts +.Dq Referer +requests. +.Pp +To obtain a CSRF token, first execute the request without the +CSRF token, and the resulting error +response will include a +.Ar X-CSRF-Token +response header. +.Pp +To execute a request with a CSRF token, first obtain a CSRF token +as described above, then copy the token to the request as the +value of the request's +.Ar X-CSRF-Token +header. +.Sh ONLINE CERTIFICATION AUTHORITY HTTP API +This service provides an HTTP-based Certification Authority (CA). +CA credentials and configuration are specified in the +.Va [bx509] +section of +.Xr krb5.conf 5 . +.Pp +The protocol consists of a +.Ar GET +of +.Ar /get-cert +with the base-63 encoding of a DER encoding of a PKCS#10 +.Ar CertificationRequest +(Certificate Signing Request, or CSR) in a +.Ar csr +required request parameter. +In a successful request, the response body will contain a PEM +encoded end entity certificate and certification chain. +.Pp +Or +.Ar GET +of +.Ar /bx509 , +as this used to be called. +.Pp +Authentication is required. +Unauthenticated requests will elicit a 401 response. +.Pp +Authorization is required. +Unauthorized requests will elicit a 403 response. +.Pp +Subject Alternative Names (SANs) and Extended Key Usage values +may be requested, both in-band in the CSR as a requested +extensions attribute, and/or via optional request parameters. +.Pp +Supported request parameters: +.Bl -tag -width Ds -offset indent +.It Li csr = Va base64-encoded-DER-encoded-CSR +.It Li dNSName = Va hostname +.It Li rfc822Name = Va email-address +.It Li xMPPName = Va XMPP-address +.It Li krb5PrincipalName = Va Kerberos-principal-name +.It Li ms-upn = Va UPN +.It Li eku = Va OID +.It Li lifetime = Va lifetime +.El +.Pp +More than one name or EKU may be requested. +.Pp +Certificate lifetimes are expressed as a decimal number and +an optional unit (which defaults to +.Dq day +). +.Sh NEGOTIATE TOKEN HTTP API +This service provides an HTTP-based Negotiate token service. +This service requires a certification authority (CA) issuer +credential as it impersonates client principals to the KDC using +PKINIT client certificates it issues itself. +.Pp +The protocol consists of a +.Ar GET +of +.Ar /get-negotiate-token +with a +.Ar target = Ar service@host +request parameter. +.Pp +In a successful request, the response body will contain a +Negotiate token for the authenticated client principal to the +requested target. +.Pp +Authentication is required. +Unauthenticated requests will elicit a 401 response. +.Pp +Subject Alternative Names (SANs) and Extended Key Usage values +may be requested, both in-band in the CSR as a requested +extensions attribute, and/or via optional request parameters. +.Pp +Supported request parameters: +.Bl -tag -width Ds -offset indent +.It Li target = Va service@hostname +.It Li redirect = Va URI +.El +.Pp +If a redirect URI is given and a matching +.Va Referer +header is included in the request, then the response will be a +redirect to that URI with the Negotiate token in an +.Va Authorization +header that the user-agent should copy to the redirected request. +.Pp +The certification authority configuration is the same as for the +.Va /get-cert +end-point, but as configured in the +.Va [get-tgt] +section of +.Xr krb5.conf 5 . +.Sh TGT HTTP API +This service provides an HTTP-based "kinit" service. +This service requires a certification authority (CA) issuer +credential as it impersonates client principals to the KDC using +PKINIT client certificates it issues itself. +.Pp +The protocol consists of a +.Ar GET +of +.Ar /get-tgt . +.Pp +Supported request parameters: +.Bl -tag -width Ds -offset indent +.It Li cname = Va principal-name +.It Li address = Va IP-address +.It Li lifetime = Va relative-time +.El +.Pp +In a successful request, the response body will contain a TGT and +its session key encoded as a "ccache" file contents. +.Pp +Authentication is required. +Unauthenticated requests will elicit a 401 response. +.Pp +Authorization is required, where the authorization check is the +same as for +.Va /get-cert +by the authenticated client principal to get a certificate with +a PKINIT SAN for itself or the requested principal if a +.Va cname +request parameter was included. +.Pp +Unauthorized requests will elicit a 403 response. +.Pp +Requested IP addresses will be added to the issued TGT if +allowed. +The IP address of the client will be included if address-less +TGTs are not allowed. +See the +.Va [get-tgt] +section of +.Xr krb5.conf 5 . +.Pp +The certification authority configuration is the same as for the +.Va /get-cert +end-point, but as configured in the +.Va [get-tgt] +section of +.Xr krb5.conf 5 . +.Sh BATCH TGT HTTP API +Some sites may have special users that operate batch jobs systems +and that can impersonate many others by obtaining TGTs for them, +and which +.Dq prestash +credentials for those users in their credentials caches. +To support these sytems, a +.Ar GET +of +.Ar /get-tgts +with multiple +.Ar cname +request parameters will return those principals' TGTs (if the +caller is authorized). +.Pp +This is similar to the +.Ar /get-tgt +end-point, but a) multiple +.Ar cname +request parameter values may be given, and b) the caller's +principal name is not used as a default for the +.Ar cname +request parameter. +The +.Ar address +and +.Ar lifetime +request parameters are honored. +.Pp +For successful +.Ar GETs +the response body is a sequence of JSON texts each of which is a +JSON object with two keys: +.Bl -tag -width Ds -offset indent +.It Ar ccache +with a base64-encoded FILE-type ccache; +.It Ar name +the name of the principal whose credentials are in that ccache. +.El +.Sh NOTES +A future release may split all these end-points into separate +services. +.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 +Configuration parameters are specified in +.Ar /etc/krb5.conf . +.Bl -tag -width Ds +.It Pa /etc/krb5.conf +.El +.\".Sh EXAMPLES +.Sh DIAGNOSTICS +See logging section of +.Nm krb5.conf.5 +.Sh SEE ALSO +.Xr krb5.conf 5 +.\".Sh STANDARDS +.\".Sh HISTORY +.\".Sh AUTHORS +.\".Sh BUGS diff --git a/third_party/heimdal/kdc/bx509d.c b/third_party/heimdal/kdc/bx509d.c new file mode 100644 index 0000000..793012b --- /dev/null +++ b/third_party/heimdal/kdc/bx509d.c @@ -0,0 +1,3175 @@ +/* + * Copyright (c) 2019 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. + */ + +/* + * This file implements a RESTful HTTPS API to an online CA, as well as an + * HTTP/Negotiate token issuer, as well as a way to get TGTs. + * + * Users are authenticated with Negotiate and/or Bearer. + * + * This is essentially a RESTful online CA sharing some code with the KDC's + * kx509 online CA, and also a proxy for PKINIT and GSS-API (Negotiate). + * + * See the manual page for HTTP API details. + * + * TBD: + * - rewrite to not use libmicrohttpd but an alternative more appropriate to + * Heimdal's license (though libmicrohttpd will do) + * - there should be an end-point for fetching an issuer's chain + * + * NOTES: + * - We use krb5_error_code values as much as possible. Where we need to use + * MHD_NO because we got that from an mhd function and cannot respond with + * an HTTP response, we use (krb5_error_code)-1, and later map that to + * MHD_NO. + * + * (MHD_NO is an ENOMEM-cannot-even-make-a-static-503-response level event.) + */ + +/* + * Theory of operation: + * + * - We use libmicrohttpd (MHD) for the HTTP(S) implementation. + * + * - MHD has an online request processing model: + * + * - all requests are handled via the `dh' and `dh_cls' closure arguments + * of `MHD_start_daemon()'; ours is called `route()' + * + * - `dh' is called N+1 times: + * - once to allocate a request context + * - once for every N chunks of request body + * - once to process the request and produce a response + * + * - the response cannot begin to be produced before consuming the whole + * request body (for requests that have a body) + * (this seems like a bug in MHD) + * + * - the response body can be produced over multiple calls (i.e., in an + * online manner) + * + * - Our `route()' processes any POST request body form data / multipart by + * treating all the key/value pairs as if they had been additional URI query + * parameters. + * + * - Then `route()' calls a handler appropriate to the URI local-part with the + * request context, and the handler produces a response in one call. + * + * I.e., we turn the online MHD request processing into not-online. Our + * handlers are presented with complete requests and must produce complete + * responses in one call. + * + * - `route()' also does any authentication and CSRF protection so that the + * request handlers don't have to. + * + * This non-online request handling approach works for most everything we want + * to do. However, for /get-tgts with very large numbers of principals, we + * might have to revisit this, using MHD_create_response_from_callback() or + * MHD_create_response_from_pipe() (and a thread to do the actual work of + * producing the body) instead of MHD_create_response_from_buffer(). + */ + +#define _XOPEN_SOURCE_EXTENDED 1 +#define _DEFAULT_SOURCE 1 +#define _BSD_SOURCE 1 +#define _GNU_SOURCE 1 + +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <ctype.h> +#include <dlfcn.h> +#include <errno.h> +#include <fcntl.h> +#include <pthread.h> +#include <signal.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <netdb.h> +#include <netinet/in.h> +#include <netinet/ip.h> + +#include <microhttpd.h> +#include "kdc_locl.h" +#include "token_validator_plugin.h" +#include <getarg.h> +#include <roken.h> +#include <krb5.h> +#include <gssapi/gssapi.h> +#include <gssapi/gssapi_krb5.h> +#include <hx509.h> +#include "../lib/hx509/hx_locl.h" +#include <hx509-private.h> + +#define heim_pcontext krb5_context +#define heim_pconfig krb5_context +#include <heimbase-svc.h> + +#if MHD_VERSION < 0x00097002 || defined(MHD_YES) +/* libmicrohttpd changed these from int valued macros to an enum in 0.9.71 */ +#ifdef MHD_YES +#undef MHD_YES +#undef MHD_NO +#endif +enum MHD_Result { MHD_NO = 0, MHD_YES = 1 }; +#define MHD_YES 1 +#define MHD_NO 0 +typedef int heim_mhd_result; +#else +typedef enum MHD_Result heim_mhd_result; +#endif + +enum k5_creds_kind { K5_CREDS_EPHEMERAL, K5_CREDS_CACHED }; + +/* + * This is to keep track of memory we need to free, mainly because we had to + * duplicate data from the MHD POST form data processor. + */ +struct free_tend_list { + void *freeme1; + void *freeme2; + struct free_tend_list *next; +}; + +/* Per-request context data structure */ +typedef struct bx509_request_desc { + /* Common elements for Heimdal request/response services */ + HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS; + + struct MHD_Connection *connection; + struct MHD_PostProcessor *pp; + struct MHD_Response *response; + krb5_times token_times; + time_t req_life; + hx509_request req; + struct free_tend_list *free_list; + const char *for_cname; + const char *target; + const char *redir; + const char *method; + size_t post_data_size; + size_t san_idx; /* For /get-tgts */ + enum k5_creds_kind cckind; + char *pkix_store; + char *tgts_filename; + FILE *tgts; + char *ccname; + char *freeme1; + char *csrf_token; + krb5_addresses tgt_addresses; /* For /get-tgt */ + char frombuf[128]; +} *bx509_request_desc; + +static void +audit_trail(bx509_request_desc r, krb5_error_code ret) +{ + const char *retname = NULL; + + /* Get a symbolic name for some error codes */ +#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(KRB5KDC_ERR_BADOPTION); + CASE(KRB5KDC_ERR_CANNOT_POSTDATE); + CASE(KRB5KDC_ERR_CLIENT_NOTYET); + CASE(KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN); + CASE(KRB5KDC_ERR_ETYPE_NOSUPP); + CASE(KRB5KDC_ERR_KEY_EXPIRED); + CASE(KRB5KDC_ERR_NAME_EXP); + CASE(KRB5KDC_ERR_NEVER_VALID); + CASE(KRB5KDC_ERR_NONE); + CASE(KRB5KDC_ERR_NULL_KEY); + CASE(KRB5KDC_ERR_PADATA_TYPE_NOSUPP); + CASE(KRB5KDC_ERR_POLICY); + CASE(KRB5KDC_ERR_PREAUTH_FAILED); + CASE(KRB5KDC_ERR_PREAUTH_REQUIRED); + CASE(KRB5KDC_ERR_SERVER_NOMATCH); + CASE(KRB5KDC_ERR_SERVICE_EXP); + CASE(KRB5KDC_ERR_SERVICE_NOTYET); + CASE(KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN); + CASE(KRB5KDC_ERR_TRTYPE_NOSUPP); + CASE(KRB5KRB_ERR_RESPONSE_TOO_BIG); + /* XXX Add relevant error codes */ + case 0: + retname = "SUCCESS"; + break; + default: + retname = NULL; + break; + } + + /* Let's save a few bytes */ + if (retname && strncmp("KRB5KDC_", retname, sizeof("KRB5KDC_") - 1) == 0) + retname += sizeof("KRB5KDC_") - 1; +#undef PREFIX + 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; + if ((ret = krb5_init_context(contextp))) + return *contextp = NULL, ret; + if (logfac) + krb5_set_log_dest(*contextp, logfac); + (void) pthread_setspecific(k5ctx, *contextp); + return *contextp ? 0 : ENOMEM; +} + +typedef enum { + CSRF_PROT_UNSPEC = 0, + CSRF_PROT_GET_WITH_HEADER = 1, + CSRF_PROT_GET_WITH_TOKEN = 2, + CSRF_PROT_POST_WITH_HEADER = 8, + CSRF_PROT_POST_WITH_TOKEN = 16, +} csrf_protection_type; + +static csrf_protection_type csrf_prot_type = CSRF_PROT_UNSPEC; +static int port = -1; +static int allow_GET_flag = -1; +static int help_flag; +static int daemonize; +static int daemon_child_fd = -1; +static int verbose_counter; +static int version_flag; +static int reverse_proxied_flag; +static int thread_per_client_flag; +struct getarg_strings audiences; +static getarg_strings csrf_prot_type_strs; +static const char *csrf_header = "X-CSRF"; +static const char *cert_file; +static const char *priv_key_file; +static const char *cache_dir; +static const char *csrf_key_file; +static char *impersonation_key_fn; + +static char csrf_key[16]; + +static krb5_error_code resp(struct bx509_request_desc *, int, + enum MHD_ResponseMemoryMode, const char *, + const void *, size_t, const char *); +static krb5_error_code bad_req(struct bx509_request_desc *, krb5_error_code, int, + const char *, ...) + HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5)); + +static krb5_error_code bad_enomem(struct bx509_request_desc *, krb5_error_code); +static krb5_error_code bad_400(struct bx509_request_desc *, krb5_error_code, char *); +static krb5_error_code bad_401(struct bx509_request_desc *, char *); +static krb5_error_code bad_403(struct bx509_request_desc *, krb5_error_code, char *); +static krb5_error_code bad_404(struct bx509_request_desc *, const char *); +static krb5_error_code bad_405(struct bx509_request_desc *, const char *); +static krb5_error_code bad_500(struct bx509_request_desc *, krb5_error_code, const char *); +static krb5_error_code bad_503(struct bx509_request_desc *, krb5_error_code, const char *); +static heim_mhd_result validate_csrf_token(struct bx509_request_desc *r); + +static int +validate_token(struct bx509_request_desc *r) +{ + krb5_error_code ret; + krb5_principal cprinc = NULL; + 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, + &cprinc, &r->token_times); + if (ret) + return bad_403(r, ret, "Token validation failed"); + if (cprinc == NULL) + return bad_403(r, ret, "Could not extract a principal name " + "from token"); + ret = krb5_unparse_name(r->context, cprinc, &r->cname); + krb5_free_principal(r->context, cprinc); + if (ret) + return bad_503(r, ret, "Could not parse principal name"); + return ret; +} + +static void +generate_key(hx509_context context, + const char *key_name, + const char *gen_type, + unsigned long gen_bits, + char **fn) +{ + struct hx509_generate_private_context *key_gen_ctx = NULL; + hx509_private_key key = NULL; + hx509_certs certs = NULL; + hx509_cert cert = NULL; + int ret; + + if (strcmp(gen_type, "rsa") != 0) + errx(1, "Only RSA keys are supported at this time"); + + if (asprintf(fn, "PEM-FILE:%s/.%s_priv_key.pem", + cache_dir, key_name) == -1 || + *fn == NULL) + err(1, "Could not set up private key for %s", key_name); + + ret = _hx509_generate_private_key_init(context, + ASN1_OID_ID_PKCS1_RSAENCRYPTION, + &key_gen_ctx); + if (ret == 0) + ret = _hx509_generate_private_key_bits(context, key_gen_ctx, gen_bits); + if (ret == 0) + ret = _hx509_generate_private_key(context, key_gen_ctx, &key); + if (ret == 0) + cert = hx509_cert_init_private_key(context, key, NULL); + if (ret == 0) + ret = hx509_certs_init(context, *fn, + HX509_CERTS_CREATE | HX509_CERTS_UNPROTECT_ALL, + NULL, &certs); + if (ret == 0) + ret = hx509_certs_add(context, certs, cert); + if (ret == 0) + ret = hx509_certs_store(context, certs, 0, NULL); + if (ret) + hx509_err(context, 1, ret, "Could not generate and save private key " + "for %s", key_name); + + _hx509_generate_private_key_free(&key_gen_ctx); + hx509_private_key_free(&key); + hx509_certs_free(&certs); + hx509_cert_free(cert); +} + +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); +} + +static krb5_error_code +mk_pkix_store(char **pkix_store) +{ + char *s = NULL; + int ret = ENOMEM; + int fd; + + if (*pkix_store) { + const char *fn = strchr(*pkix_store, ':'); + + fn = fn ? fn + 1 : *pkix_store; + (void) unlink(fn); + } + + free(*pkix_store); + *pkix_store = NULL; + if (asprintf(&s, "PEM-FILE:%s/pkix-XXXXXX", cache_dir) == -1 || + s == NULL) { + free(s); + return ret; + } + if ((fd = mkstemp(s + sizeof("PEM-FILE:") - 1)) == -1) { + free(s); + return errno; + } + (void) close(fd); + *pkix_store = s; + return 0; +} + +static krb5_error_code +resp(struct bx509_request_desc *r, + int http_status_code, + enum MHD_ResponseMemoryMode rmmode, + const char *content_type, + const void *body, + size_t bodylen, + const char *token) +{ + int mret = MHD_YES; + + if (r->response) + return MHD_YES; + + (void) gettimeofday(&r->tv_end, NULL); + if (http_status_code == MHD_HTTP_OK || + http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) + audit_trail(r, 0); + + r->response = MHD_create_response_from_buffer(bodylen, rk_UNCONST(body), + rmmode); + if (r->response == NULL) + return -1; + if (r->csrf_token) + mret = MHD_add_response_header(r->response, "X-CSRF-Token", r->csrf_token); + if (mret == MHD_YES) + mret = MHD_add_response_header(r->response, MHD_HTTP_HEADER_CACHE_CONTROL, + "no-store, max-age=0"); + if (mret == MHD_YES && http_status_code == MHD_HTTP_UNAUTHORIZED) { + mret = MHD_add_response_header(r->response, + MHD_HTTP_HEADER_WWW_AUTHENTICATE, + "Bearer"); + if (mret == MHD_YES) + mret = MHD_add_response_header(r->response, + MHD_HTTP_HEADER_WWW_AUTHENTICATE, + "Negotiate"); + } else if (mret == MHD_YES && http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) { + const char *redir; + + /* XXX Move this */ + redir = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND, + "redirect"); + mret = MHD_add_response_header(r->response, MHD_HTTP_HEADER_LOCATION, + redir); + if (mret != MHD_NO && token) + mret = MHD_add_response_header(r->response, + MHD_HTTP_HEADER_AUTHORIZATION, + token); + } + if (mret == MHD_YES && content_type) { + mret = MHD_add_response_header(r->response, + MHD_HTTP_HEADER_CONTENT_TYPE, + content_type); + } + if (mret == MHD_YES) + mret = MHD_queue_response(r->connection, http_status_code, r->response); + MHD_destroy_response(r->response); + return mret == MHD_NO ? -1 : 0; +} + +static krb5_error_code +bad_reqv(struct bx509_request_desc *r, + krb5_error_code code, + int http_status_code, + const char *fmt, + va_list ap) +{ + krb5_error_code ret; + const char *k5msg = NULL; + const char *emsg = NULL; + char *formatted = NULL; + char *msg = NULL; + + heim_audit_setkv_number((heim_svc_req_desc)r, "http-status-code", + http_status_code); + (void) gettimeofday(&r->tv_end, NULL); + if (code == ENOMEM) { + if (r->context) + krb5_log_msg(r->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); + } + + if (code) { + if (r->context) + emsg = k5msg = krb5_get_error_message(r->context, code); + else if (code > -1) + emsg = strerror(code); + else + emsg = "Unknown error"; + } + + ret = vasprintf(&formatted, fmt, ap); + if (code) { + if (ret > -1 && formatted) + ret = asprintf(&msg, "%s: %s (%d)", formatted, emsg, (int)code); + } else { + msg = formatted; + formatted = NULL; + } + heim_audit_addreason((heim_svc_req_desc)r, "%s", msg); + audit_trail(r, code); + if (r->context) + krb5_free_error_message(r->context, k5msg); + + if (ret == -1 || msg == NULL) { + if (r->context) + krb5_log_msg(r->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); + } + + ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY, + NULL, msg, strlen(msg), NULL); + free(formatted); + free(msg); + return ret == -1 ? -1 : code; +} + +static krb5_error_code +bad_req(struct bx509_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(struct bx509_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(struct bx509_request_desc *r, int ret, char *reason) +{ + return bad_req(r, ret, MHD_HTTP_BAD_REQUEST, "%s", reason); +} + +static krb5_error_code +bad_401(struct bx509_request_desc *r, char *reason) +{ + return bad_req(r, EACCES, MHD_HTTP_UNAUTHORIZED, "%s", reason); +} + +static krb5_error_code +bad_403(struct bx509_request_desc *r, krb5_error_code ret, char *reason) +{ + return bad_req(r, ret, MHD_HTTP_FORBIDDEN, "%s", reason); +} + +static krb5_error_code +bad_404(struct bx509_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(struct bx509_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_413(struct bx509_request_desc *r) +{ + return bad_req(r, E2BIG, MHD_HTTP_METHOD_NOT_ALLOWED, + "POST request body too large"); +} + +static krb5_error_code +bad_500(struct bx509_request_desc *r, + krb5_error_code ret, + const char *reason) +{ + return bad_req(r, ret, MHD_HTTP_INTERNAL_SERVER_ERROR, + "Internal error: %s", reason); +} + +static krb5_error_code +bad_503(struct bx509_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_bx509(struct bx509_request_desc *r) +{ + krb5_error_code ret; + const char *fn; + size_t bodylen; + void *body; + + /* + * This `fn' thing is just to quiet linters that think "hey, strchr() can + * return NULL so...", but here we've build `r->pkix_store' and know it has + * a ':'. + */ + if (r->pkix_store == NULL) + return bad_503(r, EINVAL, "Internal error"); /* Quiet warnings */ + fn = strchr(r->pkix_store, ':'); + fn = fn ? fn + 1 : r->pkix_store; + ret = rk_undumpdata(fn, &body, &bodylen); + if (ret) + return bad_503(r, ret, "Could not recover issued certificate " + "from PKIX store"); + + (void) gettimeofday(&r->tv_end, NULL); + ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, "application/x-pem-file", + body, bodylen, NULL); + free(body); + return ret; +} + +static heim_mhd_result +bx509_param_cb(void *d, + enum MHD_ValueKind kind, + const char *key, + const char *val) +{ + struct bx509_request_desc *r = d; + heim_oid oid = { 0, 0 }; + + if (strcmp(key, "eku") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "requested_eku", + "%s", val); + r->error_code = der_parse_heim_oid(val, ".", &oid); + if (r->error_code == 0) + r->error_code = hx509_request_add_eku(r->context->hx509ctx, r->req, &oid); + der_free_oid(&oid); + } else if (strcmp(key, "dNSName") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_dNSName", "%s", val); + r->error_code = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val); + } else if (strcmp(key, "rfc822Name") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_rfc822Name", "%s", val); + r->error_code = hx509_request_add_email(r->context->hx509ctx, r->req, val); + } else if (strcmp(key, "xMPPName") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_xMPPName", "%s", val); + r->error_code = hx509_request_add_xmpp_name(r->context->hx509ctx, r->req, + val); + } else if (strcmp(key, "krb5PrincipalName") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_krb5PrincipalName", "%s", val); + r->error_code = hx509_request_add_pkinit(r->context->hx509ctx, r->req, + val); + } else if (strcmp(key, "ms-upn") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_ms_upn", "%s", val); + r->error_code = hx509_request_add_ms_upn_name(r->context->hx509ctx, r->req, + val); + } else if (strcmp(key, "registeredID") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_registered_id", "%s", val); + r->error_code = der_parse_heim_oid(val, ".", &oid); + if (r->error_code == 0) + r->error_code = hx509_request_add_registered(r->context->hx509ctx, r->req, + &oid); + der_free_oid(&oid); + } else if (strcmp(key, "csr") == 0 && val) { + heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_csr", TRUE); + r->error_code = 0; /* Handled upstairs */ + } else if (strcmp(key, "lifetime") == 0 && val) { + r->req_life = parse_time(val, "day"); + } else { + /* Produce error for unknown params */ + heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_unknown", TRUE); + krb5_set_error_message(r->context, r->error_code = ENOTSUP, + "Query parameter %s not supported", key); + } + return r->error_code == 0 ? MHD_YES : MHD_NO /* Stop iterating */; +} + +static krb5_error_code +authorize_CSR(struct bx509_request_desc *r, + krb5_data *csr, + krb5_const_principal p) +{ + krb5_error_code ret; + + ret = hx509_request_parse_der(r->context->hx509ctx, csr, &r->req); + if (ret) + return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not parse CSR"); + r->error_code = 0; + (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND, + bx509_param_cb, r); + ret = r->error_code; + if (ret) + return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not handle query parameters"); + + ret = kdc_authorize_csr(r->context, "bx509", r->req, p); + if (ret) + return bad_403(r, ret, "Not authorized to requested certificate"); + return ret; +} + +/* + * hx509_certs_iter_f() callback to assign a private key to the first cert in a + * store. + */ +static int HX509_LIB_CALL +set_priv_key(hx509_context context, void *d, hx509_cert c) +{ + (void) _hx509_cert_assign_key(c, (hx509_private_key)d); + return -1; /* stop iteration */ +} + +static krb5_error_code +store_certs(hx509_context context, + const char *store, + hx509_certs store_these, + hx509_private_key key) +{ + krb5_error_code ret; + hx509_certs certs = NULL; + + ret = hx509_certs_init(context, store, HX509_CERTS_CREATE, NULL, + &certs); + if (ret == 0) { + if (key) + (void) hx509_certs_iter_f(context, store_these, set_priv_key, key); + hx509_certs_merge(context, certs, store_these); + } + if (ret == 0) + hx509_certs_store(context, certs, 0, NULL); + hx509_certs_free(&certs); + return ret; +} + +/* Setup a CSR for bx509() */ +static krb5_error_code +do_CA(struct bx509_request_desc *r, const char *csr) +{ + krb5_error_code ret = 0; + krb5_principal p; + hx509_certs certs = NULL; + krb5_data d; + ssize_t bytes; + char *csr2, *q; + + /* + * Work around bug where microhttpd decodes %2b to + then + to space. That + * bug does not affect other base64 special characters that get URI + * %-encoded. + */ + if ((csr2 = strdup(csr)) == NULL) + return bad_enomem(r, ENOMEM); + for (q = strchr(csr2, ' '); q; q = strchr(q + 1, ' ')) + *q = '+'; + + ret = krb5_parse_name(r->context, r->cname, &p); + if (ret) { + free(csr2); + return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not parse principal name"); + } + + /* Set CSR */ + if ((d.data = malloc(strlen(csr2))) == NULL) { + krb5_free_principal(r->context, p); + free(csr2); + return bad_enomem(r, ENOMEM); + } + + bytes = rk_base64_decode(csr2, d.data); + free(csr2); + if (bytes < 0) + ret = errno ? errno : EINVAL; + else + d.length = bytes; + if (ret) { + krb5_free_principal(r->context, p); + free(d.data); + return bad_500(r, ret, "Invalid base64 encoding of CSR"); + } + + /* + * Parses and validates the CSR, adds external extension requests from + * query parameters, then checks authorization. + */ + ret = authorize_CSR(r, &d, p); + free(d.data); + d.data = 0; + d.length = 0; + if (ret) { + krb5_free_principal(r->context, p); + return ret; /* authorize_CSR() calls bad_req() */ + } + + /* Issue the certificate */ + ret = kdc_issue_certificate(r->context, "bx509", logfac, r->req, p, + &r->token_times, r->req_life, + 1 /* send_chain */, &certs); + krb5_free_principal(r->context, p); + if (ret) { + if (ret == KRB5KDC_ERR_POLICY || ret == EACCES) + return bad_403(r, ret, + "Certificate request denied for policy reasons"); + return bad_500(r, ret, "Certificate issuance failed"); + } + + /* Setup PKIX store */ + if ((ret = mk_pkix_store(&r->pkix_store))) + return bad_500(r, ret, + "Could not create PEM store for issued certificate"); + + ret = store_certs(r->context->hx509ctx, r->pkix_store, certs, NULL); + hx509_certs_free(&certs); + if (ret) + return bad_500(r, ret, "Failed to convert issued" + " certificate and chain to PEM"); + return 0; +} + +/* Copied from kdc/connect.c */ +static void +addr_to_string(krb5_context context, + struct sockaddr *addr, + char *str, + size_t len) +{ + krb5_error_code ret; + krb5_address a; + + ret = krb5_sockaddr2address(context, addr, &a); + if (ret == 0) { + ret = krb5_print_address(&a, str, len, &len); + krb5_free_address(context, &a); + } + if (ret) + snprintf(str, len, "<family=%d>", addr->sa_family); +} + +static void clean_req_desc(struct bx509_request_desc *); + +static krb5_error_code +set_req_desc(struct MHD_Connection *connection, + const char *method, + const char *url, + struct bx509_request_desc **rp) +{ + struct bx509_request_desc *r; + const union MHD_ConnectionInfo *ci; + const char *token; + krb5_error_code ret; + + *rp = NULL; + if ((r = calloc(1, sizeof(*r))) == NULL) + return ENOMEM; + (void) gettimeofday(&r->tv_start, NULL); + + ret = get_krb5_context(&r->context); + r->connection = connection; + r->response = NULL; + r->pp = NULL; + r->request.data = "<HTTP-REQUEST>"; + r->request.length = sizeof("<HTTP-REQUEST>"); + r->from = r->frombuf; + r->tgt_addresses.len = 0; + r->tgt_addresses.val = 0; + r->hcontext = r->context ? r->context->hcontext : NULL; + r->config = NULL; + r->logf = logfac; + r->csrf_token = NULL; + r->free_list = NULL; + r->method = method; + r->reqtype = url; + r->target = r->redir = NULL; + r->pkix_store = NULL; + r->for_cname = NULL; + r->freeme1 = NULL; + r->reason = NULL; + r->tgts_filename = NULL; + r->tgts = NULL; + r->ccname = NULL; + r->reply = NULL; + r->sname = NULL; + r->cname = NULL; + r->addr = NULL; + r->req = NULL; + r->req_life = 0; + r->error_code = ret; + r->kv = heim_dict_create(10); + r->attributes = heim_dict_create(1); + if (ret == 0 && (r->kv == NULL || r->attributes == NULL)) + r->error_code = ret = ENOMEM; + 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)); + } + + heim_audit_addkv((heim_svc_req_desc)r, 0, "method", "GET"); + heim_audit_addkv((heim_svc_req_desc)r, 0, "endpoint", "%s", r->reqtype); + token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, + MHD_HTTP_HEADER_AUTHORIZATION); + if (token && r->kv) { + const char *token_end; + + if ((token_end = strchr(token, ' ')) == NULL || + (token_end - token) > INT_MAX || (token_end - token) < 2) + heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "<unknown>"); + else + heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "%.*s", + (int)(token_end - token), token); + + } + + if (ret == 0) + *rp = r; + else + clean_req_desc(r); + return ret; +} + +static void +clean_req_desc(struct bx509_request_desc *r) +{ + if (!r) + return; + while (r->free_list) { + struct free_tend_list *ftl = r->free_list; + r->free_list = r->free_list->next; + free(ftl->freeme1); + free(ftl->freeme2); + free(ftl); + } + if (r->pkix_store) { + const char *fn = strchr(r->pkix_store, ':'); + + /* + * This `fn' thing is just to quiet linters that think "hey, strchr() can + * return NULL so...", but here we've build `r->pkix_store' and know it has + * a ':'. + */ + fn = fn ? fn + 1 : r->pkix_store; + (void) unlink(fn); + } + krb5_free_addresses(r->context, &r->tgt_addresses); + hx509_request_free(&r->req); + heim_release(r->attributes); + heim_release(r->reason); + heim_release(r->kv); + if (r->ccname && r->cckind == K5_CREDS_EPHEMERAL) { + const char *fn = r->ccname; + + if (strncmp(fn, "FILE:", sizeof("FILE:") - 1) == 0) + fn += sizeof("FILE:") - 1; + (void) unlink(fn); + } + if (r->tgts) + (void) fclose(r->tgts); + if (r->tgts_filename) { + (void) unlink(r->tgts_filename); + free(r->tgts_filename); + } + /* No need to destroy r->response */ + if (r->pp) + MHD_destroy_post_processor(r->pp); + free(r->csrf_token); + free(r->pkix_store); + free(r->freeme1); + free(r->ccname); + free(r->cname); + free(r->sname); + free(r); +} + +/* Implements GETs of /bx509 */ +static krb5_error_code +bx509(struct bx509_request_desc *r) +{ + krb5_error_code ret; + const char *csr; + + /* Get required inputs */ + csr = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND, + "csr"); + if (csr == NULL) + return bad_400(r, EINVAL, "CSR is missing"); + + if (r->cname == NULL) + return bad_403(r, EINVAL, + "Could not extract principal name from token"); + + /* Parse CSR, add extensions from parameters, authorize, issue cert */ + if ((ret = do_CA(r, csr))) + return ret; + + /* Read and send the contents of the PKIX store */ + krb5_log_msg(r->context, logfac, 1, NULL, "Issued certificate to %s", + r->cname); + return good_bx509(r); +} + +/* + * princ_fs_encode_sz() and princ_fs_encode() encode a principal name to be + * safe for use as a file name. They function very much like URL encoders, but + * '~' and '.' also get encoded, and '@' does not. + * + * A corresponding decoder is not needed. + * + * XXX Maybe use krb5_cc_default_for()! + */ +static size_t +princ_fs_encode_sz(const char *in) +{ + size_t sz = strlen(in); + + while (*in) { + unsigned char c = *(const unsigned char *)(in++); + + if (isalnum(c)) + continue; + switch (c) { + case '@': + case '-': + case '_': + continue; + default: + sz += 2; + } + } + return sz; +} + +static char * +princ_fs_encode(const char *in) +{ + size_t len = strlen(in); + size_t sz = princ_fs_encode_sz(in); + size_t i, k; + char *s; + + if ((s = malloc(sz + 1)) == NULL) + return NULL; + s[sz] = '\0'; + + for (i = k = 0; i < len; i++) { + char c = in[i]; + + switch (c) { + case '@': + case '-': + case '_': + s[k++] = c; + break; + default: + if (isalnum((unsigned char)c)) { + s[k++] = c; + } else { + s[k++] = '%'; + s[k++] = "0123456789abcdef"[(c&0xff)>>4]; + s[k++] = "0123456789abcdef"[(c&0x0f)]; + } + } + } + return s; +} + + +/* + * Find an existing, live ccache for `princ' in `cache_dir' or acquire Kerberos + * creds for `princ' with PKINIT and put them in a ccache in `cache_dir'. + */ +static krb5_error_code +find_ccache(krb5_context context, const char *princ, char **ccname) +{ + krb5_error_code ret = ENOMEM; + krb5_ccache cc = NULL; + time_t life; + char *s = NULL; + + *ccname = NULL; + + /* + * Name the ccache after the principal. The principal may have special + * characters in it, such as / or \ (path component separarot), or shell + * special characters, so princ_fs_encode() it to make a ccache name. + */ + if ((s = princ_fs_encode(princ)) == NULL || + asprintf(ccname, "FILE:%s/%s.cc", cache_dir, s) == -1 || + *ccname == NULL) { + free(s); + return ENOMEM; + } + free(s); + + if ((ret = krb5_cc_resolve(context, *ccname, &cc))) { + /* krb5_cc_resolve() suceeds even if the file doesn't exist */ + free(*ccname); + *ccname = NULL; + cc = NULL; + } + + /* Check if we have a good enough credential */ + if (ret == 0 && + (ret = krb5_cc_get_lifetime(context, cc, &life)) == 0 && life > 60) { + krb5_cc_close(context, cc); + return 0; + } + if (cc) + krb5_cc_close(context, cc); + return ret ? ret : ENOENT; +} + +static krb5_error_code +get_ccache(struct bx509_request_desc *r, krb5_ccache *cc, int *won) +{ + krb5_error_code ret = 0; + char *temp_ccname = NULL; + const char *fn = NULL; + time_t life; + int fd = -1; + + /* + * Open and lock a .new ccache file. Use .new to avoid garbage files on + * crash. + * + * We can race with other threads to do this, so we loop until we + * definitively win or definitely lose the race. We win when we have a) an + * open FD that is b) flock'ed, and c) we observe with lstat() that the + * file we opened and locked is the same as on disk after locking. + * + * We don't close the FD until we're done. + * + * If we had a proper anon MEMORY ccache, we could instead use that for a + * temporary ccache, and then the initialization of and move to the final + * FILE ccache would take care to mkstemp() and rename() into place. + * fcc_open() basically does a similar thing. + */ + *cc = NULL; + *won = -1; + if (asprintf(&temp_ccname, "%s.ccnew", r->ccname) == -1 || + temp_ccname == NULL) + ret = ENOMEM; + if (ret == 0) + fn = temp_ccname + sizeof("FILE:") - 1; + if (ret == 0) do { + struct stat st1, st2; + /* + * Open and flock the temp ccache file. + * + * XXX We should really a) use _krb5_xlock(), or move that into + * lib/roken anyways, b) abstract this loop into a utility function in + * lib/roken. + */ + if (fd != -1) { + (void) close(fd); + fd = -1; + } + errno = 0; + memset(&st1, 0, sizeof(st1)); + memset(&st2, 0xff, sizeof(st2)); + if (ret == 0 && + ((fd = open(fn, O_RDWR | O_CREAT, 0600)) == -1 || + flock(fd, LOCK_EX) == -1 || + (lstat(fn, &st1) == -1 && errno != ENOENT) || + fstat(fd, &st2) == -1)) + ret = errno; + if (ret == 0 && errno == 0 && + st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino) { + if (S_ISREG(st1.st_mode)) + break; + if (unlink(fn) == -1) + ret = errno; + } + } while (ret == 0); + + /* Check if we lost any race to acquire Kerberos creds */ + if (ret == 0) + ret = krb5_cc_resolve(r->context, temp_ccname, cc); + if (ret == 0) { + ret = krb5_cc_get_lifetime(r->context, *cc, &life); + if (ret == 0 && life > 60) + *won = 0; /* We lost the race, but we win: we get to do less work */ + *won = 1; + ret = 0; + } + free(temp_ccname); + if (fd != -1) + (void) close(fd); /* Drops the flock */ + return ret; +} + +/* + * Acquire credentials for `princ' using PKINIT and the PKIX credentials in + * `pkix_store', then place the result in the ccache named `ccname' (which will + * be in our own private `cache_dir'). + * + * XXX This function could be rewritten using gss_acquire_cred_from() and + * gss_store_cred_into() provided we add new generic cred store key/value pairs + * for PKINIT. + */ +static krb5_error_code +do_pkinit(struct bx509_request_desc *r, enum k5_creds_kind kind) +{ + krb5_get_init_creds_opt *opt = NULL; + krb5_init_creds_context ctx = NULL; + krb5_error_code ret = 0; + krb5_ccache temp_cc = NULL; + krb5_ccache cc = NULL; + krb5_principal p = NULL; + const char *crealm; + const char *cname = r->for_cname ? r->for_cname : r->cname; + + if (kind == K5_CREDS_CACHED) { + int won = -1; + + ret = get_ccache(r, &temp_cc, &won); + if (ret || !won) + goto out; + /* + * We won the race to do PKINIT. Setup to acquire Kerberos creds with + * PKINIT. + * + * We should really make sure that gss_acquire_cred_from() can do this + * for us. We'd add generic cred store key/value pairs for PKIX cred + * store, trust anchors, and so on, and acquire that way, then + * gss_store_cred_into() to save it in a FILE ccache. + */ + } else { + ret = krb5_cc_new_unique(r->context, "FILE", NULL, &temp_cc); + } + + if (ret == 0) + ret = krb5_parse_name(r->context, cname, &p); + if (ret == 0) + crealm = krb5_principal_get_realm(r->context, p); + if (ret == 0) + ret = krb5_get_init_creds_opt_alloc(r->context, &opt); + if (ret == 0) + krb5_get_init_creds_opt_set_default_flags(r->context, "kinit", crealm, + opt); + if (ret == 0 && kind == K5_CREDS_EPHEMERAL && + !krb5_config_get_bool_default(r->context, NULL, TRUE, + "get-tgt", "no_addresses", NULL)) { + krb5_addresses addr; + + ret = _krb5_parse_address_no_lookup(r->context, r->frombuf, &addr); + if (ret == 0) + ret = krb5_append_addresses(r->context, &r->tgt_addresses, + &addr); + } + if (ret == 0) { + if (r->tgt_addresses.len == 0) + ret = krb5_get_init_creds_opt_set_addressless(r->context, opt, 1); + else + krb5_get_init_creds_opt_set_address_list(opt, &r->tgt_addresses); + } + if (ret == 0) + ret = krb5_get_init_creds_opt_set_pkinit(r->context, opt, p, + r->pkix_store, + NULL, /* pkinit_anchor */ + NULL, /* anchor_chain */ + NULL, /* pkinit_crl */ + 0, /* flags */ + NULL, /* prompter */ + NULL, /* prompter data */ + NULL /* password */); + if (ret == 0) + ret = krb5_init_creds_init(r->context, p, + NULL /* prompter */, + NULL /* prompter data */, + 0 /* start_time */, + opt, &ctx); + + /* + * Finally, do the AS exchange w/ PKINIT, extract the new Kerberos creds + * into temp_cc, and rename into place. Note that krb5_cc_move() closes + * the source ccache, so we set temp_cc = NULL if it succeeds. + */ + if (ret == 0) + ret = krb5_init_creds_get(r->context, ctx); + if (ret == 0) + ret = krb5_init_creds_store(r->context, ctx, temp_cc); + if (kind == K5_CREDS_CACHED) { + if (ret == 0) + ret = krb5_cc_resolve(r->context, r->ccname, &cc); + if (ret == 0) + ret = krb5_cc_move(r->context, temp_cc, cc); + if (ret == 0) + temp_cc = NULL; + } else if (ret == 0 && kind == K5_CREDS_EPHEMERAL) { + ret = krb5_cc_get_full_name(r->context, temp_cc, &r->ccname); + } + +out: + if (ctx) + krb5_init_creds_free(r->context, ctx); + krb5_get_init_creds_opt_free(r->context, opt); + krb5_free_principal(r->context, p); + krb5_cc_close(r->context, temp_cc); + krb5_cc_close(r->context, cc); + return ret; +} + +static krb5_error_code +load_priv_key(krb5_context context, const char *fn, hx509_private_key *key) +{ + hx509_private_key *keys = NULL; + krb5_error_code ret; + hx509_certs certs = NULL; + + *key = NULL; + ret = hx509_certs_init(context->hx509ctx, fn, 0, NULL, &certs); + if (ret == ENOENT) + return 0; + if (ret == 0) + ret = _hx509_certs_keys_get(context->hx509ctx, certs, &keys); + if (ret == 0 && keys[0] == NULL) + ret = ENOENT; /* XXX Better error please */ + if (ret == 0) + *key = _hx509_private_key_ref(keys[0]); + if (ret) + krb5_set_error_message(context, ret, "Could not load private " + "impersonation key from %s for PKINIT: %s", fn, + hx509_get_error_string(context->hx509ctx, ret)); + _hx509_certs_keys_free(context->hx509ctx, keys); + hx509_certs_free(&certs); + return ret; +} + +static krb5_error_code +k5_do_CA(struct bx509_request_desc *r) +{ + SubjectPublicKeyInfo spki; + hx509_private_key key = NULL; + krb5_error_code ret = 0; + krb5_principal p = NULL; + hx509_request req = NULL; + hx509_certs certs = NULL; + KeyUsage ku = int2KeyUsage(0); + const char *cname = r->for_cname ? r->for_cname : r->cname; + + memset(&spki, 0, sizeof(spki)); + ku.digitalSignature = 1; + + /* Make a CSR (halfway -- we don't need to sign it here) */ + /* XXX Load impersonation key just once?? */ + ret = load_priv_key(r->context, impersonation_key_fn, &key); + if (ret == 0) + ret = hx509_request_init(r->context->hx509ctx, &req); + if (ret == 0) + ret = krb5_parse_name(r->context, cname, &p); + if (ret == 0) + ret = hx509_private_key2SPKI(r->context->hx509ctx, key, &spki); + if (ret == 0) + hx509_request_set_SubjectPublicKeyInfo(r->context->hx509ctx, req, + &spki); + free_SubjectPublicKeyInfo(&spki); + if (ret == 0) + ret = hx509_request_add_pkinit(r->context->hx509ctx, req, cname); + if (ret == 0) + ret = hx509_request_add_eku(r->context->hx509ctx, req, + &asn1_oid_id_pkekuoid); + + /* Mark it authorized */ + if (ret == 0) + ret = hx509_request_authorize_san(req, 0); + if (ret == 0) + ret = hx509_request_authorize_eku(req, 0); + if (ret == 0) + hx509_request_authorize_ku(req, ku); + + /* Issue the certificate */ + if (ret == 0) + ret = kdc_issue_certificate(r->context, "get-tgt", logfac, req, p, + &r->token_times, r->req_life, + 1 /* send_chain */, &certs); + krb5_free_principal(r->context, p); + hx509_request_free(&req); + p = NULL; + + if (ret == KRB5KDC_ERR_POLICY || ret == EACCES) { + hx509_private_key_free(&key); + return bad_403(r, ret, + "Certificate request denied for policy reasons"); + } + if (ret == ENOMEM) { + hx509_private_key_free(&key); + return bad_503(r, ret, "Certificate issuance failed"); + } + if (ret) { + hx509_private_key_free(&key); + return bad_500(r, ret, "Certificate issuance failed"); + } + + /* Setup PKIX store and extract the certificate chain into it */ + ret = mk_pkix_store(&r->pkix_store); + if (ret == 0) + ret = store_certs(r->context->hx509ctx, r->pkix_store, certs, key); + hx509_private_key_free(&key); + hx509_certs_free(&certs); + if (ret) + return bad_500(r, ret, + "Could not create PEM store for issued certificate"); + return 0; +} + +/* Get impersonated Kerberos credentials for `cprinc' */ +static krb5_error_code +k5_get_creds(struct bx509_request_desc *r, enum k5_creds_kind kind) +{ + krb5_error_code ret; + const char *cname = r->for_cname ? r->for_cname : r->cname; + + /* If we have a live ccache for `cprinc', we're done */ + r->cckind = kind; + if (kind == K5_CREDS_CACHED && + (ret = find_ccache(r->context, cname, &r->ccname)) == 0) + return ret; /* Success */ + + /* + * Else we have to acquire a credential for them using their bearer token + * for authentication (and our keytab / initiator credentials perhaps). + */ + if ((ret = k5_do_CA(r))) + return ret; /* k5_do_CA() calls bad_req() */ + + if (ret == 0) + ret = do_pkinit(r, kind); + return ret; +} + +/* Accumulate strings */ +static void +acc_str(char **acc, char *adds, size_t addslen) +{ + char *tmp = NULL; + int l = addslen <= INT_MAX ? (int)addslen : INT_MAX; + + if (asprintf(&tmp, "%s%s%.*s", + *acc ? *acc : "", + *acc ? "; " : "", l, adds) > -1 && + tmp) { + free(*acc); + *acc = tmp; + } +} + +static char * +fmt_gss_error(OM_uint32 code, gss_OID mech) +{ + gss_buffer_desc buf; + OM_uint32 major, minor; + OM_uint32 type = mech == GSS_C_NO_OID ? GSS_C_GSS_CODE: GSS_C_MECH_CODE; + OM_uint32 more = 0; + char *r = NULL; + + do { + major = gss_display_status(&minor, code, type, mech, &more, &buf); + if (!GSS_ERROR(major)) + acc_str(&r, (char *)buf.value, buf.length); + gss_release_buffer(&minor, &buf); + } while (!GSS_ERROR(major) && more); + return r; +} + +static char * +fmt_gss_errors(const char *r, OM_uint32 major, OM_uint32 minor, gss_OID mech) +{ + char *ma, *mi, *s; + + ma = fmt_gss_error(major, GSS_C_NO_OID); + mi = mech == GSS_C_NO_OID ? NULL : fmt_gss_error(minor, mech); + if (asprintf(&s, "%s: %s%s%s", r, + ma ? ma : "Out of memory", + mi ? ": " : "", + mi ? mi : "") > -1 && + s) { + free(ma); + free(mi); + return s; + } + free(mi); + return ma; +} + +/* GSS-API error */ +static krb5_error_code +bad_req_gss(struct bx509_request_desc *r, + OM_uint32 major, + OM_uint32 minor, + gss_OID mech, + int http_status_code, + const char *reason) +{ + krb5_error_code ret; + char *msg = fmt_gss_errors(reason, major, minor, mech); + + if (major == GSS_S_BAD_NAME || major == GSS_S_BAD_NAMETYPE) + http_status_code = MHD_HTTP_BAD_REQUEST; + + if (msg) + ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY, NULL, + msg, strlen(msg), NULL); + else + ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY, NULL, + "Out of memory while formatting GSS error message", + sizeof("Out of memory while formatting GSS error message") - 1, NULL); + free(msg); + return ret; +} + +/* Make an HTTP/Negotiate token */ +static krb5_error_code +mk_nego_tok(struct bx509_request_desc *r, + char **nego_tok, + size_t *nego_toksz) +{ + gss_key_value_element_desc kv[1] = { { "ccache", r->ccname } }; + gss_key_value_set_desc store = { 1, kv }; + gss_buffer_desc token = GSS_C_EMPTY_BUFFER; + gss_buffer_desc name = GSS_C_EMPTY_BUFFER; + gss_cred_id_t cred = GSS_C_NO_CREDENTIAL; + gss_ctx_id_t ctx = GSS_C_NO_CONTEXT; + gss_name_t iname = GSS_C_NO_NAME; + gss_name_t aname = GSS_C_NO_NAME; + OM_uint32 major, minor, junk; + krb5_error_code ret; /* More like a system error code here */ + const char *cname = r->for_cname ? r->for_cname : r->cname; + char *token_b64 = NULL; + + *nego_tok = NULL; + *nego_toksz = 0; + + /* Import initiator name */ + name.length = strlen(cname); + name.value = rk_UNCONST(cname); + major = gss_import_name(&minor, &name, GSS_KRB5_NT_PRINCIPAL_NAME, &iname); + if (major != GSS_S_COMPLETE) + return bad_req_gss(r, major, minor, GSS_C_NO_OID, + MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not import cprinc parameter value as " + "Kerberos principal name"); + + /* Import target acceptor name */ + name.length = strlen(r->target); + name.value = rk_UNCONST(r->target); + major = gss_import_name(&minor, &name, GSS_C_NT_HOSTBASED_SERVICE, &aname); + if (major != GSS_S_COMPLETE) { + (void) gss_release_name(&junk, &iname); + return bad_req_gss(r, major, minor, GSS_C_NO_OID, + MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not import target parameter value as " + "Kerberos principal name"); + } + + /* Acquire a credential from the given ccache */ + major = gss_add_cred_from(&minor, cred, iname, GSS_KRB5_MECHANISM, + GSS_C_INITIATE, GSS_C_INDEFINITE, 0, &store, + &cred, NULL, NULL, NULL); + (void) gss_release_name(&junk, &iname); + if (major != GSS_S_COMPLETE) { + (void) gss_release_name(&junk, &aname); + return bad_req_gss(r, major, minor, GSS_KRB5_MECHANISM, + MHD_HTTP_FORBIDDEN, "Could not acquire credentials " + "for requested cprinc"); + } + + major = gss_init_sec_context(&minor, cred, &ctx, aname, + GSS_KRB5_MECHANISM, 0, GSS_C_INDEFINITE, + NULL, GSS_C_NO_BUFFER, NULL, &token, NULL, + NULL); + (void) gss_delete_sec_context(&junk, &ctx, GSS_C_NO_BUFFER); + (void) gss_release_name(&junk, &aname); + (void) gss_release_cred(&junk, &cred); + if (major != GSS_S_COMPLETE) + return bad_req_gss(r, major, minor, GSS_KRB5_MECHANISM, + MHD_HTTP_SERVICE_UNAVAILABLE, "Could not acquire " + "Negotiate token for requested target"); + + /* Encode token, output */ + ret = rk_base64_encode(token.value, token.length, &token_b64); + (void) gss_release_buffer(&junk, &token); + if (ret > 0) + ret = asprintf(nego_tok, "Negotiate %s", token_b64); + free(token_b64); + if (ret < 0 || *nego_tok == NULL) + return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not allocate memory for encoding Negotiate " + "token"); + *nego_toksz = ret; + return 0; +} + +static krb5_error_code +bnegotiate_get_target(struct bx509_request_desc *r) +{ + const char *target; + const char *redir; + const char *referer; /* misspelled on the wire, misspelled here, FYI */ + const char *authority; + const char *local_part; + char *s1 = NULL; + char *s2 = NULL; + + target = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND, + "target"); + redir = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND, + "redirect"); + referer = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, + MHD_HTTP_HEADER_REFERER); + if (target != NULL && redir == NULL) { + r->target = target; + return 0; + } + if (target == NULL && redir == NULL) + return bad_400(r, EINVAL, + "Query missing 'target' or 'redirect' parameter value"); + if (target != NULL && redir != NULL) + return bad_403(r, EACCES, + "Only one of 'target' or 'redirect' parameter allowed"); + if (redir != NULL && referer == NULL) + return bad_403(r, EACCES, + "Redirect request without Referer header nor allowed"); + + if (strncmp(referer, "https://", sizeof("https://") - 1) != 0 || + strncmp(redir, "https://", sizeof("https://") - 1) != 0) + return bad_403(r, EACCES, + "Redirect requests permitted only for https referrers"); + + /* Parse out authority from each URI, redirect and referrer */ + authority = redir + sizeof("https://") - 1; + if ((local_part = strchr(authority, '/')) == NULL) + local_part = authority + strlen(authority); + if ((s1 = strndup(authority, local_part - authority)) == NULL) + return bad_enomem(r, ENOMEM); + + authority = referer + sizeof("https://") - 1; + if ((local_part = strchr(authority, '/')) == NULL) + local_part = authority + strlen(authority); + if ((s2 = strndup(authority, local_part - authority)) == NULL) { + free(s1); + return bad_enomem(r, ENOMEM); + } + + /* Both must match */ + if (strcasecmp(s1, s2) != 0) { + free(s2); + free(s1); + return bad_403(r, EACCES, "Redirect request does not match referer"); + } + free(s2); + + if (strchr(s1, '@')) { + free(s1); + return bad_403(r, EACCES, + "Redirect request authority has login information"); + } + + /* Extract hostname portion of authority and format GSS name */ + if (strchr(s1, ':')) + *strchr(s1, ':') = '\0'; + if (asprintf(&r->freeme1, "HTTP@%s", s1) == -1 || r->freeme1 == NULL) { + free(s1); + return bad_enomem(r, ENOMEM); + } + + r->target = r->freeme1; + r->redir = redir; + free(s1); + return 0; +} + +/* + * Implements /bnegotiate end-point. + * + * Query parameters (mutually exclusive): + * + * - target=<name> + * - redirect=<URL-encoded-URL> + * + * If the redirect query parameter is set then the Referer: header must be as + * well, and the authority of the redirect and Referer URIs must be the same. + */ +static krb5_error_code +bnegotiate(struct bx509_request_desc *r) +{ + krb5_error_code ret; + size_t nego_toksz = 0; + char *nego_tok = NULL; + + ret = bnegotiate_get_target(r); + if (ret) + return ret; /* bnegotiate_get_target() calls bad_req() */ + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "target", "%s", + r->target ? r->target : "<unknown>"); + heim_audit_setkv_bool((heim_svc_req_desc)r, "redir", !!r->redir); + + /* + * Make sure we have Kerberos credentials for cprinc. If we have them + * cached from earlier, this will be fast (all local), else it will involve + * taking a file lock and talking to the KDC using kx509 and PKINIT. + * + * Perhaps we could use S4U instead, which would speed up the slow path a + * bit. + */ + ret = k5_get_creds(r, K5_CREDS_CACHED); + if (ret) + return bad_403(r, ret, + "Could not acquire Kerberos credentials using PKINIT"); + + /* Acquire the Negotiate token and output it */ + if (ret == 0 && r->ccname != NULL) + ret = mk_nego_tok(r, &nego_tok, &nego_toksz); + + if (ret == 0) { + /* Look ma', Negotiate as an OAuth-like token system! */ + if (r->redir) + ret = resp(r, MHD_HTTP_TEMPORARY_REDIRECT, MHD_RESPMEM_PERSISTENT, + NULL, "", 0, nego_tok); + else + ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, + "application/x-negotiate-token", nego_tok, nego_toksz, + NULL); + } + + free(nego_tok); + return ret; +} + +static krb5_error_code +authorize_TGT_REQ(struct bx509_request_desc *r) +{ + krb5_principal p = NULL; + krb5_error_code ret; + const char *for_cname = r->for_cname ? r->for_cname : r->cname; + + if (for_cname == r->cname || strcmp(r->cname, r->for_cname) == 0) + return 0; + + ret = hx509_request_init(r->context->hx509ctx, &r->req); + if (ret) + return bad_500(r, ret, "Out of resources"); + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_krb5PrincipalName", "%s", for_cname); + ret = hx509_request_add_eku(r->context->hx509ctx, r->req, + ASN1_OID_ID_PKEKUOID); + if (ret == 0) + ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req, + for_cname); + if (ret == 0) + ret = krb5_parse_name(r->context, r->cname, &p); + if (ret == 0) + ret = kdc_authorize_csr(r->context, "get-tgt", r->req, p); + krb5_free_principal(r->context, p); + hx509_request_free(&r->req); + r->req = NULL; + if (ret) + return bad_403(r, ret, "Not authorized to requested TGT"); + return ret; +} + +static heim_mhd_result +get_tgt_param_cb(void *d, + enum MHD_ValueKind kind, + const char *key, + const char *val) +{ + struct bx509_request_desc *r = d; + + if (strcmp(key, "address") == 0 && val) { + if (!krb5_config_get_bool_default(r->context, NULL, + FALSE, + "get-tgt", "allow_addresses", NULL)) { + krb5_set_error_message(r->context, r->error_code = ENOTSUP, + "Query parameter %s not allowed", key); + } else { + krb5_addresses addresses; + + r->error_code = _krb5_parse_address_no_lookup(r->context, val, + &addresses); + if (r->error_code == 0) + r->error_code = krb5_append_addresses(r->context, &r->tgt_addresses, + &addresses); + krb5_free_addresses(r->context, &addresses); + } + } else if (strcmp(key, "cname") == 0) { + /* Handled upstairs */ + ; + } else if (strcmp(key, "lifetime") == 0 && val) { + r->req_life = parse_time(val, "day"); + } else { + /* Produce error for unknown params */ + heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_unknown", TRUE); + krb5_set_error_message(r->context, r->error_code = ENOTSUP, + "Query parameter %s not supported", key); + } + return r->error_code == 0 ? MHD_YES : MHD_NO /* Stop iterating */; +} + +/* + * Implements /get-tgt end-point. + * + * Query parameters: + * + * - cname=<name> (client principal name, if not the same as the authenticated + * name, then this will be impersonated if allowed; may be + * given only once) + * + * - address=<IP> (IP address to add as a ticket address; may be given + * multiple times) + * + * - lifetime=<time> (requested lifetime for the ticket; may be given only + * once) + */ +static krb5_error_code +get_tgt(struct bx509_request_desc *r) +{ + krb5_error_code ret; + size_t bodylen; + const char *fn; + void *body; + + r->for_cname = MHD_lookup_connection_value(r->connection, + MHD_GET_ARGUMENT_KIND, "cname"); + if (r->for_cname && r->for_cname[0] == '\0') + r->for_cname = NULL; + ret = authorize_TGT_REQ(r); + if (ret) + return ret; /* authorize_TGT_REQ() calls bad_req() */ + + r->error_code = 0; + (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND, + get_tgt_param_cb, r); + ret = r->error_code; + + /* k5_get_creds() calls bad_req() */ + if (ret == 0) + ret = k5_get_creds(r, K5_CREDS_EPHEMERAL); + if (ret) + return bad_403(r, ret, + "Could not acquire Kerberos credentials using PKINIT"); + + fn = strchr(r->ccname, ':'); + if (fn == NULL) + return bad_500(r, ret, "Impossible error"); + fn++; + if ((errno = rk_undumpdata(fn, &body, &bodylen))) + return bad_503(r, ret, "Could not get TGT"); + + ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, + "application/x-krb5-ccache", body, bodylen, NULL); + free(body); + return ret; +} + +static int +get_tgts_accumulate_ccache_write_json(struct bx509_request_desc *r, + krb5_error_code code, + const char *data, + size_t datalen) +{ + heim_object_t k, v; + heim_string_t text; + heim_error_t e = NULL; + heim_dict_t o; + int ret; + + o = heim_dict_create(9); + k = heim_string_create("name"); + v = heim_string_create(r->for_cname); + if (o && k && v) + ret = heim_dict_set_value(o, k, v); + else + ret = ENOMEM; + + if (ret == 0) { + heim_release(v); + heim_release(k); + k = heim_string_create("error_code"); + v = heim_number_create(code); + if (k && v) + ret = heim_dict_set_value(o, k, v); + } + if (ret == 0 && data != NULL) { + heim_release(v); + heim_release(k); + k = heim_string_create("ccache"); + v = heim_data_create(data, datalen); + if (k && v) + ret = heim_dict_set_value(o, k, v); + } + if (ret == 0 && code != 0) { + const char *s = krb5_get_error_message(r->context, code); + + heim_release(v); + heim_release(k); + k = heim_string_create("error"); + v = heim_string_create(s ? s : "Out of memory"); + krb5_free_error_message(r->context, s); + if (k && v) + ret = heim_dict_set_value(o, k, v); + } + heim_release(v); + heim_release(k); + if (ret) { + heim_release(o); + return bad_503(r, errno, "Out of memory"); + } + + text = heim_json_copy_serialize(o, + HEIM_JSON_F_NO_DATA_DICT | + HEIM_JSON_F_ONE_LINE, + &e); + if (text) { + const char *s = heim_string_get_utf8(text); + + (void) fwrite(s, strlen(s), 1, r->tgts); + } else { + const char *s = NULL; + v = heim_error_copy_string(e); + if (v) + s = heim_string_get_utf8(v); + if (s == NULL) + s = "<unknown encoder error>"; + krb5_log_msg(r->context, logfac, 1, NULL, "Failed to encode JSON text with ccache or error for %s: %s", + r->for_cname, s); + heim_release(v); + } + heim_release(text); + heim_release(o); + return MHD_YES; +} + +/* Writes one ccache to a response file, as JSON */ +static int +get_tgts_accumulate_ccache(struct bx509_request_desc *r, krb5_error_code ret) +{ + const char *fn; + size_t bodylen = 0; + void *body = NULL; + int res; + + if (r->tgts == NULL) { + int fd = -1; + + if (asprintf(&r->tgts_filename, + "%s/tgts-json-XXXXXX", cache_dir) == -1 || + r->tgts_filename == NULL) { + free(r->tgts_filename); + r->tgts_filename = NULL; + + return bad_enomem(r, r->error_code = ENOMEM); + } + if ((fd = mkstemp(r->tgts_filename)) == -1) + return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE, + "%s", strerror(r->error_code = errno)); + if ((r->tgts = fdopen(fd, "w+")) == NULL) { + (void) close(fd); + return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE, + "%s", strerror(r->error_code = errno)); + } + } + + if (ret == 0) { + fn = strchr(r->ccname, ':'); + if (fn == NULL) + return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE, + "Internal error (invalid credentials cache name)"); + fn++; + if ((r->error_code = rk_undumpdata(fn, &body, &bodylen))) + return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE, + "%s", strerror(r->error_code)); + (void) unlink(fn); + free(r->ccname); + r->ccname = NULL; + if (bodylen > INT_MAX >> 4) { + free(body); + return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE, + "Credentials cache too large!"); + } + } + + res = get_tgts_accumulate_ccache_write_json(r, ret, body, bodylen); + free(body); + return res; +} + +static heim_mhd_result +get_tgts_param_authorize_cb(void *d, + enum MHD_ValueKind kind, + const char *key, + const char *val) +{ + struct bx509_request_desc *r = d; + krb5_error_code ret = 0; + + if (strcmp(key, "cname") != 0 || val == NULL) + return MHD_YES; + + if (r->req == NULL) { + ret = hx509_request_init(r->context->hx509ctx, &r->req); + if (ret == 0) + ret = hx509_request_add_eku(r->context->hx509ctx, r->req, + ASN1_OID_ID_PKEKUOID); + if (ret) + return bad_500(r, ret, "Out of resources"); + } + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_krb5PrincipalName", "%s", val); + ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req, + val); + if (ret) + return bad_403(r, ret, "Not authorized to requested TGT"); + return MHD_YES; +} + +/* For each requested principal, produce a ccache */ +static heim_mhd_result +get_tgts_param_execute_cb(void *d, + enum MHD_ValueKind kind, + const char *key, + const char *val) +{ + struct bx509_request_desc *r = d; + hx509_san_type san_type; + krb5_error_code ret; + size_t san_idx = r->san_idx++; + const char *save_for_cname = r->for_cname; + char *s = NULL; + int res; + + /* We expect only cname=principal q-params here */ + if (strcmp(key, "cname") != 0 || val == NULL) + return MHD_YES; + + /* + * We expect the `san_idx'th SAN in the `r->req' request checked by + * kdc_authorize_csr() to be the same as this cname. This happens + * naturally because we add these SANs to `r->req' in the same order as we + * visit them here (unless our HTTP library somehow went crazy). + * + * Still, we check that it's the same SAN. + */ + ret = hx509_request_get_san(r->req, san_idx, &san_type, &s); + if (ret == HX509_NO_ITEM || + san_type != HX509_SAN_TYPE_PKINIT || + strcmp(s, val) != 0) { + /* + * If the cname and SAN don't match, it's some weird internal error + * (can't happen). + */ + krb5_set_error_message(r->context, r->error_code = EACCES, + "PKINIT SAN not granted: %s (internal error)", + val); + ret = EACCES; + } + + /* + * We're going to pretend to be this SAN for the purpose of acquring a TGT + * for it. So we "push" `r->for_cname'. + */ + if (ret == 0) + r->for_cname = val; + + /* + * Our authorizer supports partial authorization where the whole request is + * rejected but some features of it are permitted. + * + * (In most end-points we don't want partial authorization, but in + * /get-tgts we very much do.) + */ + if (ret == 0 && !hx509_request_san_authorized_p(r->req, san_idx)) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "REJECT_krb5PrincipalName", "%s", val); + krb5_set_error_message(r->context, r->error_code = EACCES, + "PKINIT SAN denied: %s", val); + ret = EACCES; + } + if (ret == 0) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "ACCEPT_krb5PrincipalName", "%s", val); + ret = k5_get_creds(r, K5_CREDS_EPHEMERAL); + if (ret == 0) + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "ISSUE_krb5PrincipalName", "%s", val); + } + + /* + * If ret == 0 this will gather the TGT we acquired, else it will acquire + * the error we got. + */ + res = get_tgts_accumulate_ccache(r, ret); + + /* Now we "pop" `r->for_cname' */ + r->for_cname = save_for_cname; + + hx509_xfree(s); + return res; +} + +/* + * Implements /get-tgts end-point. + * + * Query parameters: + * + * - cname=<name> (client principal name, if not the same as the authenticated + * name, then this will be impersonated if allowed; may be + * given multiple times) + */ +static krb5_error_code +get_tgts(struct bx509_request_desc *r) +{ + krb5_error_code ret; + krb5_principal p = NULL; + size_t bodylen; + void *body; + int res = MHD_YES; + + /* Prep to authorize */ + ret = krb5_parse_name(r->context, r->cname, &p); + if (ret) + return bad_403(r, ret, "Could not parse caller principal name"); + if (ret == 0) { + /* Extract q-params other than `cname' */ + r->error_code = 0; + res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND, + get_tgt_param_cb, r); + if (r->response || res == MHD_NO) { + krb5_free_principal(r->context, p); + return res; + } + + ret = r->error_code; + } + if (ret == 0) { + /* + * Check authorization of the authenticated client to the requested + * client principal names (calls bad_req()). + */ + r->error_code = 0; + res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND, + get_tgts_param_authorize_cb, r); + if (r->response || res == MHD_NO) { + krb5_free_principal(r->context, p); + return res; + } + + ret = r->error_code; + if (ret == 0) { + /* Use the same configuration as /get-tgt (or should we?) */ + ret = kdc_authorize_csr(r->context, "get-tgt", r->req, p); + + /* + * We tolerate EACCES because we support partial approval. + * + * (KRB5_PLUGIN_NO_HANDLE means no plugin handled the authorization + * check.) + */ + if (ret == EACCES || ret == KRB5_PLUGIN_NO_HANDLE) + ret = 0; + if (ret) { + krb5_free_principal(r->context, p); + return bad_403(r, ret, "Permission denied"); + } + } + } + if (ret == 0) { + /* + * Get the actual TGTs that were authorized. + * + * get_tgts_param_execute_cb() calls bad_req() + */ + r->error_code = 0; + res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND, + get_tgts_param_execute_cb, r); + if (r->response || res == MHD_NO) { + krb5_free_principal(r->context, p); + return res; + } + ret = r->error_code; + } + krb5_free_principal(r->context, p); + hx509_request_free(&r->req); + r->req = NULL; + + /* + * get_tgts_param_execute_cb() will write its JSON response to the file + * named by r->ccname. + */ + if (fflush(r->tgts) != 0) + return bad_503(r, ret, "Could not get TGT"); + if ((errno = rk_undumpdata(r->tgts_filename, &body, &bodylen))) + return bad_503(r, ret, "Could not get TGT"); + + ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, + "application/x-krb5-ccaches-json", body, bodylen, NULL); + free(body); + return ret; +} + +static krb5_error_code +health(const char *method, struct bx509_request_desc *r) +{ + if (strcmp(method, "HEAD") == 0) + return resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL); + return resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL, + "To determine the health of the service, use the /bx509 " + "end-point.\n", + sizeof("To determine the health of the service, use the " + "/bx509 end-point.\n") - 1, NULL); + +} + +static krb5_error_code +mac_csrf_token(struct bx509_request_desc *r, krb5_storage *sp) +{ + krb5_error_code ret; + krb5_data data; + char mac[EVP_MAX_MD_SIZE]; + unsigned int maclen = sizeof(mac); + HMAC_CTX *ctx = NULL; + + ret = krb5_storage_to_data(sp, &data); + if (ret == 0 && (ctx = HMAC_CTX_new()) == NULL) + ret = krb5_enomem(r->context); + /* HMAC the token body and the client principal name */ + if (ret == 0) { + if (HMAC_Init_ex(ctx, csrf_key, sizeof(csrf_key), + EVP_sha256(), + NULL) == 0) { + HMAC_CTX_cleanup(ctx); + ret = krb5_enomem(r->context); + } else { + HMAC_Update(ctx, data.data, data.length); + if (r->cname) + HMAC_Update(ctx, r->cname, strlen(r->cname)); + HMAC_Final(ctx, mac, &maclen); + HMAC_CTX_cleanup(ctx); + krb5_data_free(&data); + data.length = maclen; + data.data = mac; + if (krb5_storage_write(sp, mac, maclen) != maclen) + ret = krb5_enomem(r->context); + } + } + if (ctx) + HMAC_CTX_free(ctx); + return ret; +} + +/* + * Make a CSRF token. If one is also given, make one with the same body + * content so we can check the HMAC. + * + * Outputs the token and its age. Do not use either if the token does not + * equal the given token. + */ +static krb5_error_code +make_csrf_token(struct bx509_request_desc *r, + const char *given, + char **token, + int64_t *age) +{ + 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); + + /* Extract issue time and nonce from token */ + 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 && + rk_base64_encode(data.data, data.length, token) < 0) + ret = errno; + krb5_storage_free(sp); + krb5_data_free(&data); + return ret; +} + +static heim_mhd_result +validate_csrf_token(struct bx509_request_desc *r) +{ + const char *given; + int64_t age; + krb5_error_code ret; + + if ((((csrf_prot_type & CSRF_PROT_GET_WITH_HEADER) && + strcmp(r->method, "GET") == 0) || + ((csrf_prot_type & CSRF_PROT_POST_WITH_HEADER) && + strcmp(r->method, "POST") == 0)) && + MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, + csrf_header) == NULL) { + ret = bad_req(r, EACCES, MHD_HTTP_FORBIDDEN, + "Request must have header \"%s\"", csrf_header); + return ret == -1 ? MHD_NO : MHD_YES; + } + + if (strcmp(r->method, "GET") == 0 && + !(csrf_prot_type & CSRF_PROT_GET_WITH_TOKEN)) + return 0; + if (strcmp(r->method, "POST") == 0 && + !(csrf_prot_type & CSRF_PROT_POST_WITH_TOKEN)) + return 0; + + given = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, + "X-CSRF-Token"); + ret = make_csrf_token(r, given, &r->csrf_token, &age); + if (ret) + return bad_503(r, ret, "Could not make or validate CSRF token"); + if (given == NULL) + return bad_req(r, EACCES, MHD_HTTP_FORBIDDEN, + "CSRF token needed; copy the X-CSRF-Token: response " + "header to your next POST"); + if (strlen(given) != strlen(r->csrf_token) || + strcmp(given, r->csrf_token) != 0) + return bad_403(r, EACCES, "Invalid CSRF token"); + if (age > 300) + return bad_403(r, EACCES, "CSRF token expired"); + return 0; +} + +/* + * MHD callback to free the request context when MHD is done sending the + * response. + */ +static void +cleanup_req(void *cls, + struct MHD_Connection *connection, + void **con_cls, + enum MHD_RequestTerminationCode toe) +{ + struct bx509_request_desc *r = *con_cls; + + (void)cls; + (void)connection; + (void)toe; + clean_req_desc(r); + *con_cls = NULL; +} + +/* Callback for MHD POST form data processing */ +static heim_mhd_result +ip(void *cls, + enum MHD_ValueKind kind, + const char *key, + const char *content_name, + const char *content_type, + const char *transfer_encoding, + const char *val, + uint64_t off, + size_t size) +{ + struct bx509_request_desc *r = cls; + struct free_tend_list *ftl = calloc(1, sizeof(*ftl)); + char *keydup = strdup(key); + char *valdup = strndup(val, size); + + (void)content_name; /* MIME attachment name */ + (void)content_type; /* Don't care -- MHD liked it */ + (void)transfer_encoding; + (void)off; /* Offset in POST data */ + + /* + * We're going to MHD_set_connection_value(), but we need copies because + * the MHD POST processor quite naturally keeps none of the chunks + * received. + */ + if (ftl == NULL || keydup == NULL || valdup == NULL) { + free(ftl); + free(keydup); + free(valdup); + return MHD_NO; + } + ftl->freeme1 = keydup; + ftl->freeme2 = valdup; + ftl->next = r->free_list; + r->free_list = ftl; + + return MHD_set_connection_value(r->connection, MHD_GET_ARGUMENT_KIND, + keydup, valdup); +} + +typedef krb5_error_code (*handler)(struct bx509_request_desc *); + +struct route { + const char *local_part; + handler h; + unsigned int referer_ok:1; +} routes[] = { + { "/get-cert", bx509, 0 }, + { "/get-negotiate-token", bnegotiate, 1 }, + { "/get-tgt", get_tgt, 0 }, + { "/get-tgts", get_tgts, 0 }, + /* Lousy old names to be removed eventually */ + { "/bnegotiate", bnegotiate, 1 }, + { "/bx509", bx509, 0 }, +}; + +/* + * We should commonalize all of: + * + * - route() and related infrastructure + * - including the CSRF functions + * - and Negotiate/Bearer authentication + * + * so that we end up with a simple framework that our daemons can invoke to + * serve simple functions that take a fully-consumed request and send a + * response. + * + * Then: + * + * - split out the CA and non-CA bits into separate daemons using that common + * code, + * - make httpkadmind use that common code, + * - abstract out all the MHD stuff. + */ + +/* Routes requests */ +static heim_mhd_result +route(void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **ctx) +{ + struct bx509_request_desc *r = *ctx; + size_t i; + int ret; + + if (r == 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 want to make sure to send any 401s early, so we check + * WWW-Authenticate now, not later. + * + * We'll get called again to really do the processing. If we're + * handling a POST then we'll also get called with upload_data != NULL, + * possibly multiple times. + */ + if ((ret = set_req_desc(connection, method, url, &r))) + return MHD_NO; + *ctx = r; + + /* All requests other than /health require authentication */ + if (strcmp(url, "/health") == 0) + return MHD_YES; + + /* + * Authenticate and do CSRF protection. + * + * If the Referer: header is set in the request, we don't want CSRF + * protection as only /get-negotiate-token will accept a Referer: + * header (see routes[] and below), so we'll call validate_csrf_token() + * for the other routes or reject the request for having Referer: set. + */ + ret = validate_token(r); + if (ret == 0 && + MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, "Referer") == NULL) + ret = validate_csrf_token(r); + + /* + * As this is the initial call to this handler, we must return now. + * + * If authentication or CSRF protection failed then we'll already have + * enqueued a 401, 403, or 5xx response and then we're done. + * + * If both authentication and CSRF protection succeeded then no + * response has been queued up and we'll get called again to finally + * process the request, then this entire if block will not be executed. + */ + return ret == -1 ? MHD_NO : MHD_YES; + } + + /* Validate HTTP method */ + if (strcmp(method, "GET") != 0 && + strcmp(method, "POST") != 0 && + strcmp(method, "HEAD") != 0) { + return bad_405(r, method) == -1 ? MHD_NO : MHD_YES; + } + + if ((strcmp(method, "HEAD") == 0 || strcmp(method, "GET") == 0) && + (strcmp(url, "/health") == 0 || strcmp(url, "/") == 0)) { + /* /health end-point -- no authentication, no CSRF, no nothing */ + return health(method, r) == -1 ? MHD_NO : MHD_YES; + } + + if (r->cname == NULL) + return bad_401(r, "Authorization token is missing"); + + if (strcmp(method, "POST") == 0 && *upload_data_size != 0) { + /* + * Consume all the POST body and set form data as MHD_GET_ARGUMENT_KIND + * (as if they had been URI query parameters). + * + * We have to do this before we can MHD_queue_response() as MHD will + * not consume the rest of the request body on its own, so it's an + * error to MHD_queue_response() before we've done this, and if we do + * then MHD just closes the connection. + * + * 4KB should be more than enough buffer space for all the keys we + * expect. + */ + if (r->pp == NULL) + r->pp = MHD_create_post_processor(connection, 4096, ip, r); + if (r->pp == NULL) { + ret = bad_503(r, errno ? errno : ENOMEM, + "Could not consume POST data"); + return ret == -1 ? MHD_NO : MHD_YES; + } + if (r->post_data_size + *upload_data_size > 1UL<<17) { + return bad_413(r) == -1 ? MHD_NO : MHD_YES; + } + r->post_data_size += *upload_data_size; + if (MHD_post_process(r->pp, upload_data, + *upload_data_size) == MHD_NO) { + ret = bad_503(r, errno ? errno : ENOMEM, + "Could not consume POST data"); + return ret == -1 ? MHD_NO : MHD_YES; + } + *upload_data_size = 0; + return MHD_YES; + } + + /* + * Either this is a HEAD, a GET, or a POST whose request body has now been + * received completely and processed. + */ + + /* Allow GET? */ + if (strcmp(method, "GET") == 0 && !allow_GET_flag) { + /* No */ + return bad_405(r, method) == -1 ? MHD_NO : MHD_YES; + } + + for (i = 0; i < sizeof(routes)/sizeof(routes[0]); i++) { + if (strcmp(url, routes[i].local_part) != 0) + continue; + if (!routes[i].referer_ok && + MHD_lookup_connection_value(r->connection, + MHD_HEADER_KIND, + "Referer") != NULL) { + ret = bad_req(r, EACCES, MHD_HTTP_FORBIDDEN, + "GET from browser not allowed"); + return ret == -1 ? MHD_NO : MHD_YES; + } + if (strcmp(method, "HEAD") == 0) + ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL, "", 0, + NULL); + else + ret = routes[i].h(r); + return ret == -1 ? MHD_NO : MHD_YES; + } + + ret = bad_404(r, url); + 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)", "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" }, + { "port", 'p', arg_integer, &port, "port number (default: 443)", "PORT" }, + { "cache-dir", 0, arg_string, &cache_dir, + "cache directory", "DIRECTORY" }, + { "allow-GET", 0, arg_negative_flag, &allow_GET_flag, NULL, NULL }, + { "csrf-header", 0, arg_flag, + &csrf_header, "required request header", "HEADER-NAME" }, + { "csrf-protection-type", 0, arg_strings, &csrf_prot_type_strs, + "Anti-CSRF protection type", "TYPE" }, + { "csrf-key-file", 0, arg_string, &csrf_key_file, + "CSRF MAC key", "FILE" }, + { "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", "use thread per-client" }, + { "verbose", 'v', arg_counter, &verbose_counter, "verbose", "run verbosely" } +}; + +static int +usage(int e) +{ + arg_printusage(args, sizeof(args) / sizeof(args[0]), "bx509", + "\nServes RESTful GETs of /get-cert, /get-tgt, /get-tgts, and\n" + "/get-negotiate-toke, performing corresponding kx509 and, \n" + "possibly, PKINIT requests to the KDCs of the requested \n" + "realms (or just the given REALM).\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 +bx509_openlog(krb5_context context, + const char *svc, + krb5_log_facility **fac) +{ + char **s = NULL, **p; + + krb5_initlog(context, "bx509d", 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 +} + +static void +get_csrf_prot_type(krb5_context context) +{ + char * const *strs = csrf_prot_type_strs.strings; + size_t n = csrf_prot_type_strs.num_strings; + size_t i; + char **freeme = NULL; + + if (csrf_header == NULL) + csrf_header = krb5_config_get_string(context, NULL, "bx509d", + "csrf_protection_csrf_header", + NULL); + + if (n == 0) { + char * const *p; + + strs = freeme = krb5_config_get_strings(context, NULL, "bx509d", + "csrf_protection_type", NULL); + for (p = strs; p && p; p++) + n++; + } + + for (i = 0; i < n; i++) { + if (strcmp(strs[i], "GET-with-header") == 0) + csrf_prot_type |= CSRF_PROT_GET_WITH_HEADER; + else if (strcmp(strs[i], "GET-with-token") == 0) + csrf_prot_type |= CSRF_PROT_GET_WITH_TOKEN; + else if (strcmp(strs[i], "POST-with-header") == 0) + csrf_prot_type |= CSRF_PROT_POST_WITH_HEADER; + else if (strcmp(strs[i], "POST-with-token") == 0) + csrf_prot_type |= CSRF_PROT_POST_WITH_TOKEN; + } + free(freeme); + + /* + * For GETs we default to no CSRF protection as our GETable resources are + * safe and idempotent and we count on the browser not to make the + * responses available to cross-site requests. + * + * But, really, we don't want browsers even making these requests since, if + * the browsers behave correctly, then there's no point, and if they don't + * behave correctly then that could be catastrophic. Of course, there's no + * guarantee that a browser won't have other catastrophic bugs, but still, + * we should probably change this default in the future: + * + * if (!(csrf_prot_type & CSRF_PROT_GET_WITH_HEADER) && + * !(csrf_prot_type & CSRF_PROT_GET_WITH_TOKEN)) + * csrf_prot_type |= <whatever-the-new-default-should-be>; + */ + + /* + * For POSTs we default to CSRF protection with anti-CSRF tokens even + * though out POSTable resources are safe and idempotent when POSTed and we + * could count on the browser not to make the responses available to + * cross-site requests. + */ + if (!(csrf_prot_type & CSRF_PROT_POST_WITH_HEADER) && + !(csrf_prot_type & CSRF_PROT_POST_WITH_TOKEN)) + csrf_prot_type |= CSRF_PROT_POST_WITH_TOKEN; +} + +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; + char *priv_key_pem = NULL; + char *cert_pem = NULL; + char sig; + int optidx = 0; + int ret; + + setprogname("bx509d"); + 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 ((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"); + + bx509_openlog(context, "bx509d", &logfac); + krb5_set_log_dest(context, logfac); + load_plugins(context); + + if (allow_GET_flag == -1) + warnx("It is safer to use --no-allow-GET"); + + get_csrf_prot_type(context); + + krb5_generate_random_block((void *)&csrf_key, sizeof(csrf_key)); + if (csrf_key_file == NULL) + csrf_key_file = krb5_config_get_string(context, NULL, "bx509d", + "csrf_key_file", NULL); + if (csrf_key_file) { + ssize_t bytes; + int fd; + + fd = open(csrf_key_file, O_RDONLY); + if (fd == -1) + err(1, "CSRF key file missing %s", csrf_key_file); + bytes = read(fd, csrf_key, sizeof(csrf_key)); + if (bytes == -1) + err(1, "Could not read CSRF key file %s", csrf_key_file); + if (bytes != sizeof(csrf_key)) + errx(1, "CSRF key file too small (should be %lu) %s", + (unsigned long)sizeof(csrf_key), csrf_key_file); + } + + if (audiences.num_strings == 0) { + char localhost[MAXHOSTNAMELEN]; + + ret = gethostname(localhost, sizeof(localhost)); + if (ret == -1) + errx(1, "Could not determine local hostname; use --audience"); + + if ((audiences.strings = + calloc(1, sizeof(audiences.strings[0]))) == NULL || + (audiences.strings[0] = strdup(localhost)) == NULL) + err(1, "Out of memory"); + audiences.num_strings = 1; + } + + if (daemonize && daemon_child_fd == -1) + daemon_child_fd = roken_detach_prep(argc, argv, "--daemon-child"); + daemonize = 0; + + argc -= optidx; + argv += optidx; + if (argc != 0) + usage(1); + + if (cache_dir == NULL) { + char *s = NULL; + + if (asprintf(&s, "%s/bx509d-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); + } + + generate_key(context->hx509ctx, "impersonation", "rsa", 2048, &impersonation_key_fn); + +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. + */ + (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, + /* + * This is a connection access callback. We + * don't use it. + */ + NULL, NULL, + /* This is our request handler */ + route, (char *)NULL, + MHD_OPTION_SOCK_ADDR, &sin, + MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200, + MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10, + /* This is our request cleanup handler */ + MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL, + MHD_OPTION_END); + } else if (sock != MHD_INVALID_SOCKET) { + /* + * Restart following a possible certificate/key rollover, reusing 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_NOTIFY_COMPLETED, cleanup_req, NULL, + MHD_OPTION_LISTEN_SOCKET, sock, + MHD_OPTION_END); + sock = MHD_INVALID_SOCKET; + } else { + /* + * Initial MHD_start_daemon(), with TLS. + * + * Subsequently we'll restart reusing the listen socket this creates. + * See above. + */ + 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_NOTIFY_COMPLETED, cleanup_req, NULL, + MHD_OPTION_END); + } + if (current == NULL) + err(1, "Could not start bx509 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/third_party/heimdal/kdc/ca.c b/third_party/heimdal/kdc/ca.c new file mode 100644 index 0000000..4402c44 --- /dev/null +++ b/third_party/heimdal/kdc/ca.c @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2019 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. + */ + +#include "kdc_locl.h" +#include <hex.h> +#include <rfc2459_asn1.h> +#include "../lib/hx509/hx_locl.h" + +#include <stdarg.h> + +/* + * This file implements a singular utility function `kdc_issue_certificate()' + * for certificate issuance for kx509 and bx509, which takes a principal name, + * an `hx509_request' resulting from parsing a CSR and possibly adding + * SAN/EKU/KU extensions, the start/end times of request's authentication + * method, and whether to include a full certificate chain in the result. + */ + +/* + * Get a configuration sub-tree for kx509 based on what's being requested and + * by whom. + * + * We have a number of cases: + * + * - default certificate (no CSR used, or no certificate extensions requested) + * - for client principals + * - for service principals + * - client certificate requested (CSR used and client-y SANs/EKUs requested) + * - server certificate requested (CSR used and server-y SANs/EKUs requested) + * - mixed client/server certificate requested (...) + */ +static krb5_error_code +get_cf(krb5_context context, + const char *app_name, + krb5_log_facility *logf, + hx509_request req, + krb5_principal cprinc, + const krb5_config_binding **cf) +{ + krb5_error_code ret = ENOTSUP; + const char *realm = krb5_principal_get_realm(context, cprinc); + + *cf = NULL; + if (strcmp(app_name, "kdc") == 0) + *cf = krb5_config_get_list(context, NULL, app_name, "realms", realm, + "kx509", NULL); + else + *cf = krb5_config_get_list(context, NULL, app_name, "realms", realm, + NULL); + if (*cf) + ret = 0; + if (ret) { + krb5_log_msg(context, logf, 3, NULL, + "No %s configuration for certification authority [%s] " + "realm %s -> kx509 -> ...", app_name, + strcmp(app_name, "bx509") == 0 ? "bx509" : "kx509", + realm); + krb5_set_error_message(context, KRB5KDC_ERR_POLICY, + "No %s configuration for certification authority [%s] " + "realm %s -> kx509 -> ...", app_name, + strcmp(app_name, "bx509") == 0 ? "bx509" : "kx509", + realm); + } + return ret; +} + +/* + * Build a certifate for `principal' and its CSR. + */ +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +kdc_issue_certificate(krb5_context context, + const char *app_name, + krb5_log_facility *logf, + hx509_request req, + krb5_principal cprinc, + krb5_times *auth_times, + time_t req_life, + int send_chain, + hx509_certs *out) +{ + const krb5_config_binding *cf; + krb5_error_code ret = KRB5KDC_ERR_POLICY; + KRB5PrincipalName cprinc2; + + *out = NULL; + cprinc2.principalName = cprinc->name; + cprinc2.realm = cprinc->realm; + + /* Get configuration */ + ret = get_cf(context, app_name, logf, req, cprinc, &cf); + if (ret == 0) + ret = _hx509_ca_issue_certificate(context->hx509ctx, + (const heim_config_binding *)cf, + logf, req, &cprinc2, + auth_times->starttime, + auth_times->endtime, + req_life, + send_chain, + out); + if (ret == EACCES) + ret = KRB5KDC_ERR_POLICY; + return ret; +} diff --git a/third_party/heimdal/kdc/cjwt_token_validator.c b/third_party/heimdal/kdc/cjwt_token_validator.c new file mode 100644 index 0000000..93742e5 --- /dev/null +++ b/third_party/heimdal/kdc/cjwt_token_validator.c @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2019 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. + */ + +/* + * This is a plugin by which bx509d can validate JWT Bearer tokens using the + * cjwt library. + * + * Configuration: + * + * [kdc] + * realm = { + * A.REALM.NAME = { + * cjwt_jqk = PATH-TO-JWK-PEM-FILE + * } + * } + * + * where AUDIENCE-FOR-KDC is the value of the "audience" (i.e., the target) of + * the token. + */ + +#include <config.h> +#include <errno.h> +#include <stdio.h> +#include <string.h> +#include <string.h> +#include <heimbase.h> +#include <krb5.h> +#include <common_plugin.h> +#include <hdb.h> +#include <roken.h> +#include <token_validator_plugin.h> +#include <cjwt/cjwt.h> +#ifdef HAVE_CJSON +#include <cJSON.h> +#endif + +static const char * +get_kv(krb5_context context, const char *realm, const char *k, const char *k2) +{ + return krb5_config_get_string(context, NULL, "bx509", "realms", realm, + k, k2, NULL); +} + +static krb5_error_code +get_issuer_pubkeys(krb5_context context, + const char *realm, + krb5_data *previous, + krb5_data *current, + krb5_data *next) +{ + krb5_error_code save_ret = 0; + krb5_error_code ret; + const char *v; + size_t nkeys = 0; + + previous->data = current->data = next->data = 0; + previous->length = current->length = next->length = 0; + + if ((v = get_kv(context, realm, "cjwt_jwk_next", NULL)) && + (++nkeys) && + (ret = rk_undumpdata(v, &next->data, &next->length))) + save_ret = ret; + if ((v = get_kv(context, realm, "cjwt_jwk_previous", NULL)) && + (++nkeys) && + (ret = rk_undumpdata(v, &previous->data, &previous->length)) && + save_ret == 0) + save_ret = ret; + if ((v = get_kv(context, realm, "cjwt_jwk_current", NULL)) && + (++nkeys) && + (ret = rk_undumpdata(v, ¤t->data, ¤t->length)) && + save_ret == 0) + save_ret = ret; + if (nkeys == 0) + krb5_set_error_message(context, EINVAL, "jwk issuer key not specified in " + "[bx509]->realm->%s->cjwt_jwk_{previous,current,next}", + realm); + if (!previous->length && !current->length && !next->length) + krb5_set_error_message(context, save_ret, + "Could not read jwk issuer public key files"); + if (current->length && current->length == next->length && + memcmp(current->data, next->data, next->length) == 0) { + free(next->data); + next->data = 0; + next->length = 0; + } + if (current->length && current->length == previous->length && + memcmp(current->data, previous->data, previous->length) == 0) { + free(previous->data); + previous->data = 0; + previous->length = 0; + } + + if (previous->data == NULL && current->data == NULL && next->data == NULL) + return krb5_set_error_message(context, ENOENT, "No JWKs found"), + ENOENT; + return 0; +} + +static krb5_error_code +check_audience(krb5_context context, + const char *realm, + cjwt_t *jwt, + const char * const *audiences, + size_t naudiences) +{ + size_t i, k; + + if (!jwt->aud) { + krb5_set_error_message(context, EACCES, "JWT bearer token has no " + "audience"); + return EACCES; + } + for (i = 0; i < jwt->aud->count; i++) + for (k = 0; k < naudiences; k++) + if (strcasecmp(audiences[k], jwt->aud->names[i]) == 0) + return 0; + krb5_set_error_message(context, EACCES, "JWT bearer token's audience " + "does not match any expected audience"); + return EACCES; +} + +static krb5_error_code +get_princ(krb5_context context, + const char *realm, + cjwt_t *jwt, + krb5_principal *actual_principal) +{ + krb5_error_code ret; + const char *force_realm = NULL; + const char *domain; + +#ifdef HAVE_CJSON + if (jwt->private_claims) { + cJSON *jval; + + if ((jval = cJSON_GetObjectItem(jwt->private_claims, "authz_sub"))) + return krb5_parse_name(context, jval->valuestring, actual_principal); + } +#endif + + if (jwt->sub == NULL) { + krb5_set_error_message(context, EACCES, "JWT token lacks 'sub' " + "(subject name)!"); + return EACCES; + } + if ((domain = strchr(jwt->sub, '@'))) { + force_realm = get_kv(context, realm, "cjwt_force_realm", ++domain); + ret = krb5_parse_name(context, jwt->sub, actual_principal); + } else { + ret = krb5_parse_name_flags(context, jwt->sub, + KRB5_PRINCIPAL_PARSE_NO_REALM, + actual_principal); + } + if (ret) + krb5_set_error_message(context, ret, "JWT token 'sub' not a valid " + "principal name: %s", jwt->sub); + else if (force_realm) + ret = krb5_principal_set_realm(context, *actual_principal, realm); + else if (domain == NULL) + ret = krb5_principal_set_realm(context, *actual_principal, realm); + /* else leave the domain as the realm */ + return ret; +} + +static KRB5_LIB_CALL krb5_error_code +validate(void *ctx, + krb5_context context, + const char *realm, + const char *token_type, + krb5_data *token, + const char * const *audiences, + size_t naudiences, + krb5_boolean *result, + krb5_principal *actual_principal, + krb5_times *token_times) +{ + heim_octet_string jwk_previous; + heim_octet_string jwk_current; + heim_octet_string jwk_next; + cjwt_t *jwt = NULL; + char *tokstr = NULL; + char *defrealm = NULL; + int ret; + + if (strcmp(token_type, "Bearer") != 0) + return KRB5_PLUGIN_NO_HANDLE; /* Not us */ + + if ((tokstr = calloc(1, token->length + 1)) == NULL) + return ENOMEM; + memcpy(tokstr, token->data, token->length); + + if (realm == NULL) { + ret = krb5_get_default_realm(context, &defrealm); + if (ret) { + krb5_set_error_message(context, ret, "could not determine default " + "realm"); + free(tokstr); + return ret; + } + realm = defrealm; + } + + ret = get_issuer_pubkeys(context, realm, &jwk_previous, &jwk_current, + &jwk_next); + if (ret) { + free(defrealm); + free(tokstr); + return ret; + } + + if (jwk_current.length && jwk_current.data) + ret = cjwt_decode(tokstr, 0, &jwt, jwk_current.data, + jwk_current.length); + if (ret && jwk_next.length && jwk_next.data) + ret = cjwt_decode(tokstr, 0, &jwt, jwk_next.data, + jwk_next.length); + if (ret && jwk_previous.length && jwk_previous.data) + ret = cjwt_decode(tokstr, 0, &jwt, jwk_previous.data, + jwk_previous.length); + free(jwk_previous.data); + free(jwk_current.data); + free(jwk_next.data); + jwk_previous.data = jwk_current.data = jwk_next.data = NULL; + free(tokstr); + tokstr = NULL; + switch (ret) { + case 0: + if (jwt == NULL) { + krb5_set_error_message(context, EINVAL, "JWT validation failed"); + free(defrealm); + return EPERM; + } + if (jwt->header.alg == alg_none) { + krb5_set_error_message(context, EINVAL, "JWT signature algorithm " + "not supported"); + free(defrealm); + return EPERM; + } + break; + case -1: + krb5_set_error_message(context, EINVAL, "invalid JWT format"); + free(defrealm); + return EINVAL; + case -2: + krb5_set_error_message(context, EINVAL, "JWT signature validation " + "failed (wrong issuer?)"); + free(defrealm); + return EPERM; + default: + krb5_set_error_message(context, ret, "misc token validation error"); + free(defrealm); + return ret; + } + + /* Success; check audience */ + if ((ret = check_audience(context, realm, jwt, audiences, naudiences))) { + cjwt_destroy(&jwt); + free(defrealm); + return EACCES; + } + + /* Success; extract principal name */ + if ((ret = get_princ(context, realm, jwt, actual_principal)) == 0) { + token_times->authtime = jwt->iat.tv_sec; + token_times->starttime = jwt->nbf.tv_sec; + token_times->endtime = jwt->exp.tv_sec; + token_times->renew_till = jwt->exp.tv_sec; + *result = TRUE; + } + + cjwt_destroy(&jwt); + free(defrealm); + return ret; +} + +static KRB5_LIB_CALL krb5_error_code +hcjwt_init(krb5_context context, void **c) +{ + *c = NULL; + return 0; +} + +static KRB5_LIB_CALL void +hcjwt_fini(void *c) +{ +} + +static krb5plugin_token_validator_ftable plug_desc = + { 1, hcjwt_init, hcjwt_fini, validate }; + +static krb5plugin_token_validator_ftable *plugs[] = { &plug_desc }; + +static uintptr_t +hcjwt_get_instance(const char *libname) +{ + if (strcmp(libname, "krb5") == 0) + return krb5_get_instance(libname); + return 0; +} + +krb5_plugin_load_ft kdc_token_validator_plugin_load; + +krb5_error_code KRB5_CALLCONV +kdc_token_validator_plugin_load(heim_pcontext context, + krb5_get_instance_func_t *get_instance, + size_t *num_plugins, + krb5_plugin_common_ftable_cp **plugins) +{ + *get_instance = hcjwt_get_instance; + *num_plugins = sizeof(plugs) / sizeof(plugs[0]); + *plugins = (krb5_plugin_common_ftable_cp *)plugs; + return 0; +} diff --git a/third_party/heimdal/kdc/config.c b/third_party/heimdal/kdc/config.c new file mode 100644 index 0000000..9fd3686 --- /dev/null +++ b/third_party/heimdal/kdc/config.c @@ -0,0 +1,329 @@ +/* + * Copyright (c) 1997-2007 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2009 Apple Inc. 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. + */ + +#include "kdc_locl.h" +#include <getarg.h> +#include <parse_bytes.h> + +#define MAX_REQUEST_MAX 67108864ll /* 64MB, the maximum accepted value of max_request */ + +struct dbinfo { + char *realm; + char *dbname; + char *mkey_file; + struct dbinfo *next; +}; + +static char *config_file; /* location of kdc config file */ + +static int require_preauth = -1; /* 1 == require preauth for all principals */ +static char *max_request_str; /* `max_request' as a string */ + +static int disable_des = -1; + +static int builtin_hdb_flag; +int testing_flag; +static int help_flag; +static int version_flag; + +/* Should we enable the HTTP hack? */ +int enable_http = -1; + +/* Log over requests to the KDC */ +const char *request_log; + +/* A string describing on what ports to listen */ +const char *port_str; + +krb5_addresses explicit_addresses; + +size_t max_request_udp; +size_t max_request_tcp; + + +static struct getarg_strings addresses_str; /* addresses to listen on */ + +char *runas_string; +char *chroot_string; + + +static struct getargs args[] = { + { + "config-file", 'c', arg_string, &config_file, + "location of config file", "file" + }, + { + "require-preauth", 'p', arg_negative_flag, &require_preauth, + "don't require pa-data in as-reqs", NULL + }, + { + "max-request", 0, arg_string, &max_request_str, + "max size for a kdc-request", "size" + }, + { "enable-http", 'H', arg_flag, &enable_http, "turn on HTTP support", + NULL }, + { "ports", 'P', arg_string, rk_UNCONST(&port_str), + "ports to listen to", "portspec" + }, + { + "detach", 0 , arg_flag, &detach_from_console, + "detach from console", NULL + }, + { + "daemon-child", 0 , arg_flag, &daemon_child, + "private argument, do not use", NULL + }, +#ifdef __APPLE__ + { + "bonjour", 0 , arg_flag, &do_bonjour, + "private argument, do not use", NULL + }, +#endif + { "addresses", 0, arg_strings, &addresses_str, + "addresses to listen on", "list of addresses" }, + { "disable-des", 0, arg_flag, &disable_des, + "disable DES", NULL }, + { "builtin-hdb", 0, arg_flag, &builtin_hdb_flag, + "list builtin hdb backends", NULL}, + { "runas-user", 0, arg_string, &runas_string, + "run as this user when connected to network", NULL + }, + { "chroot", 0, arg_string, &chroot_string, + "chroot directory to run in", NULL + }, + { "testing", 0, arg_flag, &testing_flag, NULL, NULL }, + { "help", 'h', arg_flag, &help_flag, NULL, NULL }, + { "version", 'v', arg_flag, &version_flag, NULL, NULL } +}; + +static int num_args = sizeof(args) / sizeof(args[0]); + +static void +usage(int ret) +{ + arg_printusage (args, num_args, NULL, ""); + exit (ret); +} + +static void +add_one_address (krb5_context context, const char *str, int first) +{ + krb5_error_code ret; + krb5_addresses tmp; + + ret = krb5_parse_address (context, str, &tmp); + if (ret) + krb5_err (context, 1, ret, "parse_address `%s'", str); + if (first) + krb5_copy_addresses(context, &tmp, &explicit_addresses); + else + krb5_append_addresses(context, &explicit_addresses, &tmp); + krb5_free_addresses (context, &tmp); +} + +krb5_kdc_configuration * +configure(krb5_context context, int argc, char **argv, int *optidx) +{ + krb5_kdc_configuration *config; + krb5_error_code ret; + + const char *p; + + *optidx = 0; + + while (getarg(args, num_args, argc, argv, optidx)) + warnx("error at argument `%s'", argv[*optidx]); + + if (help_flag) + usage (0); + + if (version_flag) { + print_version(NULL); + exit(0); + } + + if (builtin_hdb_flag) { + char *list; + ret = hdb_list_builtin(context, &list); + if (ret) + krb5_err(context, 1, ret, "listing builtin hdb backends"); + printf("builtin hdb backends: %s\n", list); + free(list); + exit(0); + } + + if(detach_from_console == -1) + detach_from_console = krb5_config_get_bool_default(context, NULL, + FALSE, + "kdc", + "detach", NULL); + + if (detach_from_console && daemon_child == -1) + daemon_child = roken_detach_prep(argc, argv, "--daemon-child"); + + { + char **files; + int aret; + + if (config_file == NULL) { + aret = asprintf(&config_file, "%s/kdc.conf", hdb_db_dir(context)); + if (aret == -1 || config_file == NULL) + errx(1, "out of memory"); + } + + ret = krb5_prepend_config_files_default(config_file, &files); + if (ret) + krb5_err(context, 1, ret, "getting configuration files"); + + ret = krb5_set_config_files(context, files); + krb5_free_config_files(files); + if(ret) + krb5_err(context, 1, ret, "reading configuration files"); + } + + ret = krb5_kdc_get_config(context, &config); + if (ret) + krb5_err(context, 1, ret, "krb5_kdc_default_config"); + + kdc_openlog(context, "kdc", config); + + ret = krb5_kdc_set_dbinfo(context, config); + if (ret) + krb5_err(context, 1, ret, "krb5_kdc_set_dbinfo"); + + if (max_request_str) { + int64_t bytes; + + if ((bytes = parse_bytes(max_request_str, NULL)) < 0) + krb5_errx(context, 1, "--max-request must be non-negative"); + + if (bytes > MAX_REQUEST_MAX) + krb5_errx(context, 1, "--max-request size is too big " + "(must be smaller than %lld)", MAX_REQUEST_MAX); + + max_request_tcp = max_request_udp = bytes; + } + + if(max_request_tcp == 0){ + p = krb5_config_get_string (context, + NULL, + "kdc", + "max-request", + NULL); + if (p) { + int64_t bytes; + + if ((bytes = parse_bytes(max_request_str, NULL)) < 0) + krb5_errx(context, 1, "[kdc] max-request must be non-negative"); + + if (bytes > MAX_REQUEST_MAX) + krb5_errx(context, 1, "[kdc] max-request size is too big " + "(must be smaller than %lld)", MAX_REQUEST_MAX); + + max_request_tcp = max_request_udp = bytes; + } + } + + if(require_preauth != -1) + config->require_preauth = require_preauth; + + if(port_str == NULL){ + p = krb5_config_get_string(context, NULL, "kdc", "ports", NULL); + if (p != NULL) + port_str = strdup(p); + } + + explicit_addresses.len = 0; + + if (addresses_str.num_strings) { + int i; + + for (i = 0; i < addresses_str.num_strings; ++i) + add_one_address (context, addresses_str.strings[i], i == 0); + free_getarg_strings (&addresses_str); + } else { + char **foo = krb5_config_get_strings (context, NULL, + "kdc", "addresses", NULL); + + if (foo != NULL) { + add_one_address (context, *foo++, TRUE); + while (*foo) + add_one_address (context, *foo++, FALSE); + } + } + + if(enable_http == -1) + enable_http = krb5_config_get_bool(context, NULL, "kdc", + "enable-http", NULL); + + if(request_log == NULL) + request_log = krb5_config_get_string(context, NULL, + "kdc", + "kdc-request-log", + NULL); + + if (krb5_config_get_string(context, NULL, "kdc", + "enforce-transited-policy", NULL)) + krb5_errx(context, 1, "enforce-transited-policy deprecated, " + "use [kdc]transited-policy instead"); + + if(max_request_tcp == 0) + max_request_tcp = 64 * 1024; + if(max_request_udp == 0) + max_request_udp = 64 * 1024; + + if (port_str == NULL) + port_str = "+"; + + if(disable_des == -1) + disable_des = krb5_config_get_bool_default(context, NULL, + FALSE, + "kdc", + "disable-des", NULL); + if(disable_des) { + krb5_enctype_disable(context, ETYPE_DES_CBC_CRC); + krb5_enctype_disable(context, ETYPE_DES_CBC_MD4); + krb5_enctype_disable(context, ETYPE_DES_CBC_MD5); + krb5_enctype_disable(context, ETYPE_DES_CBC_NONE); + krb5_enctype_disable(context, ETYPE_DES_CFB64_NONE); + krb5_enctype_disable(context, ETYPE_DES_PCBC_NONE); + } + + krb5_kdc_plugin_init(context); + + krb5_kdc_pkinit_config(context, config); + + return config; +} diff --git a/third_party/heimdal/kdc/connect.c b/third_party/heimdal/kdc/connect.c new file mode 100644 index 0000000..ba8c8ad --- /dev/null +++ b/third_party/heimdal/kdc/connect.c @@ -0,0 +1,1319 @@ +/* + * Copyright (c) 1997-2005 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. + */ + +#include "kdc_locl.h" + +/* + * a tuple describing on what to listen + */ + +struct port_desc{ + int family; + int type; + int port; +}; + +/* the current ones */ + +static struct port_desc *ports; +static size_t num_ports; +static pid_t bonjour_pid = -1; + +/* + * add `family, port, protocol' to the list with duplicate suppresion. + */ + +static void +add_port(krb5_context context, + int family, int port, const char *protocol) +{ + int type; + size_t i; + + if(strcmp(protocol, "udp") == 0) + type = SOCK_DGRAM; + else if(strcmp(protocol, "tcp") == 0) + type = SOCK_STREAM; + else + return; + for(i = 0; i < num_ports; i++){ + if(ports[i].type == type + && ports[i].port == port + && ports[i].family == family) + return; + } + ports = realloc(ports, (num_ports + 1) * sizeof(*ports)); + if (ports == NULL) + krb5_err (context, 1, errno, "realloc"); + ports[num_ports].family = family; + ports[num_ports].type = type; + ports[num_ports].port = port; + num_ports++; +} + +/* + * add a triple but with service -> port lookup + * (this prints warnings for stuff that does not exist) + */ + +static void +add_port_service(krb5_context context, + int family, const char *service, int port, + const char *protocol) +{ + port = krb5_getportbyname (context, service, protocol, port); + add_port (context, family, port, protocol); +} + +/* + * add the port with service -> port lookup or string -> number + * (no warning is printed) + */ + +static void +add_port_string (krb5_context context, + int family, const char *str, const char *protocol) +{ + struct servent *sp; + int port; + + sp = roken_getservbyname (str, protocol); + if (sp != NULL) { + port = sp->s_port; + } else { + char *end; + + port = htons(strtol(str, &end, 0)); + if (end == str) + return; + } + add_port (context, family, port, protocol); +} + +/* + * add the standard collection of ports for `family' + */ + +static void +add_standard_ports (krb5_context context, + krb5_kdc_configuration *config, + int family) +{ + add_port_service(context, family, "kerberos", 88, "udp"); + add_port_service(context, family, "kerberos", 88, "tcp"); + add_port_service(context, family, "kerberos-sec", 88, "udp"); + add_port_service(context, family, "kerberos-sec", 88, "tcp"); + if(enable_http) + add_port_service(context, family, "http", 80, "tcp"); + if(config->enable_kx509) { + add_port_service(context, family, "kca_service", 9878, "udp"); + add_port_service(context, family, "kca_service", 9878, "tcp"); + } + +} + +/* + * parse the set of space-delimited ports in `str' and add them. + * "+" => all the standard ones + * otherwise it's port|service[/protocol] + */ + +static void +parse_ports(krb5_context context, + krb5_kdc_configuration *config, + const char *str) +{ + char *pos = NULL; + char *p; + char *str_copy = strdup (str); + + p = strtok_r(str_copy, " \t", &pos); + while(p != NULL) { + if(strcmp(p, "+") == 0) { +#ifdef HAVE_IPV6 + add_standard_ports(context, config, AF_INET6); +#endif + add_standard_ports(context, config, AF_INET); + } else { + char *q = strchr(p, '/'); + if(q){ + *q++ = 0; +#ifdef HAVE_IPV6 + add_port_string(context, AF_INET6, p, q); +#endif + add_port_string(context, AF_INET, p, q); + }else { +#ifdef HAVE_IPV6 + add_port_string(context, AF_INET6, p, "udp"); + add_port_string(context, AF_INET6, p, "tcp"); +#endif + add_port_string(context, AF_INET, p, "udp"); + add_port_string(context, AF_INET, p, "tcp"); + } + } + + p = strtok_r(NULL, " \t", &pos); + } + free (str_copy); +} + +/* + * every socket we listen on + */ + +struct descr { + krb5_socket_t s; + int type; + int port; + unsigned char *buf; + size_t size; + size_t len; + time_t timeout; + struct sockaddr_storage __ss; + struct sockaddr *sa; + socklen_t sock_len; + char addr_string[128]; +}; + +static void +init_descr(struct descr *d) +{ + memset(d, 0, sizeof(*d)); + d->sa = (struct sockaddr *)&d->__ss; + d->s = rk_INVALID_SOCKET; +} + +/* + * re-initialize all `n' ->sa in `d'. + */ + +static void +reinit_descrs (struct descr *d, int n) +{ + int i; + + for (i = 0; i < n; ++i) + d[i].sa = (struct sockaddr *)&d[i].__ss; +} + +/* + * Create the socket (family, type, port) in `d' + */ + +static void +init_socket(krb5_context context, + krb5_kdc_configuration *config, + struct descr *d, krb5_address *a, int family, int type, int port) +{ + krb5_error_code ret; + struct sockaddr_storage __ss; + struct sockaddr *sa = (struct sockaddr *)&__ss; + krb5_socklen_t sa_size = sizeof(__ss); + + init_descr (d); + + ret = krb5_addr2sockaddr (context, a, sa, &sa_size, port); + if (ret) { + krb5_warn(context, ret, "krb5_addr2sockaddr"); + rk_closesocket(d->s); + d->s = rk_INVALID_SOCKET; + return; + } + + if (sa->sa_family != family) + return; + + d->s = socket(family, type, 0); + if(rk_IS_BAD_SOCKET(d->s)){ + krb5_warn(context, errno, "socket(%d, %d, 0)", family, type); + d->s = rk_INVALID_SOCKET; + return; + } + rk_cloexec(d->s); +#if defined(HAVE_SETSOCKOPT) && defined(SOL_SOCKET) && defined(SO_REUSEADDR) + { + int one = 1; + (void) setsockopt(d->s, SOL_SOCKET, SO_REUSEADDR, (void *)&one, + sizeof(one)); + } +#endif + d->type = type; + d->port = port; + + socket_set_nonblocking(d->s, 1); + + if(rk_IS_SOCKET_ERROR(bind(d->s, sa, sa_size))){ + char a_str[256]; + size_t len; + + krb5_print_address (a, a_str, sizeof(a_str), &len); + krb5_warn(context, errno, "bind %s/%d", a_str, ntohs(port)); + rk_closesocket(d->s); + d->s = rk_INVALID_SOCKET; + return; + } + if(type == SOCK_STREAM && rk_IS_SOCKET_ERROR(listen(d->s, SOMAXCONN))){ + char a_str[256]; + size_t len; + + krb5_print_address (a, a_str, sizeof(a_str), &len); + krb5_warn(context, errno, "listen %s/%d", a_str, ntohs(port)); + rk_closesocket(d->s); + d->s = rk_INVALID_SOCKET; + return; + } + socket_set_keepalive(d->s, 1); +} + +/* + * Allocate descriptors for all the sockets that we should listen on + * and return the number of them. + */ + +static int +init_sockets(krb5_context context, + krb5_kdc_configuration *config, + struct descr **desc) +{ + krb5_error_code ret; + size_t i, j; + struct descr *d; + int num = 0; + krb5_addresses addresses; + + if (explicit_addresses.len) { + addresses = explicit_addresses; + } else { + ret = krb5_get_all_server_addrs (context, &addresses); + if (ret) + krb5_err (context, 1, ret, "krb5_get_all_server_addrs"); + } + parse_ports(context, config, port_str); + d = malloc(addresses.len * num_ports * sizeof(*d)); + if (d == NULL) + krb5_errx(context, 1, "malloc(%lu) failed", + (unsigned long)num_ports * sizeof(*d)); + + for (i = 0; i < num_ports; i++){ + for (j = 0; j < addresses.len; ++j) { + init_socket(context, config, &d[num], &addresses.val[j], + ports[i].family, ports[i].type, ports[i].port); + if(d[num].s != rk_INVALID_SOCKET){ + char a_str[80]; + size_t len; + + krb5_print_address (&addresses.val[j], a_str, + sizeof(a_str), &len); + + kdc_log(context, config, 3, "listening on %s port %u/%s", + a_str, + ntohs(ports[i].port), + (ports[i].type == SOCK_STREAM) ? "tcp" : "udp"); + /* XXX */ + num++; + } + } + } + krb5_free_addresses (context, &addresses); + d = realloc(d, num * sizeof(*d)); + if (d == NULL && num != 0) + krb5_errx(context, 1, "realloc(%lu) failed", + (unsigned long)num * sizeof(*d)); + reinit_descrs (d, num); + *desc = d; + return num; +} + +/* + * + */ + +static const char * +descr_type(struct descr *d) +{ + if (d->type == SOCK_DGRAM) + return "udp"; + else if (d->type == SOCK_STREAM) + return "tcp"; + return "unknown"; +} + +static void +addr_to_string(krb5_context context, + struct sockaddr *addr, size_t addr_len, char *str, size_t len) +{ + krb5_address a; + if(krb5_sockaddr2address(context, addr, &a) == 0) { + if(krb5_print_address(&a, str, len, &len) == 0) { + krb5_free_address(context, &a); + return; + } + krb5_free_address(context, &a); + } + snprintf(str, len, "<family=%d>", addr->sa_family); +} + +/* + * + */ + +static void +send_reply(krb5_context context, + krb5_kdc_configuration *config, + krb5_boolean prependlength, + struct descr *d, + krb5_data *reply) +{ + kdc_log(context, config, 4, + "sending %lu bytes to %s", (unsigned long)reply->length, + d->addr_string); + if(prependlength){ + unsigned char l[4]; + l[0] = (reply->length >> 24) & 0xff; + l[1] = (reply->length >> 16) & 0xff; + l[2] = (reply->length >> 8) & 0xff; + l[3] = reply->length & 0xff; + if(rk_IS_SOCKET_ERROR(sendto(d->s, l, sizeof(l), 0, d->sa, d->sock_len))) { + kdc_log (context, config, + 1, "sendto(%s): %s", d->addr_string, + strerror(rk_SOCK_ERRNO)); + return; + } + } + if(rk_IS_SOCKET_ERROR(sendto(d->s, reply->data, reply->length, 0, d->sa, d->sock_len))) { + kdc_log (context, config, 1, "sendto(%s): %s", d->addr_string, + strerror(rk_SOCK_ERRNO)); + return; + } +} + +/* + * Handle the request in `buf, len' to socket `d' + */ + +static void +do_request(krb5_context context, + krb5_kdc_configuration *config, + void *buf, size_t len, krb5_boolean prependlength, + struct descr *d) +{ + krb5_error_code ret; + krb5_data reply; + int datagram_reply = (d->type == SOCK_DGRAM); + + krb5_kdc_update_time(NULL); + + krb5_data_zero(&reply); + ret = krb5_kdc_process_request(context, config, + buf, len, &reply, &prependlength, + d->addr_string, d->sa, + datagram_reply); + if(request_log) + krb5_kdc_save_request(context, request_log, buf, len, &reply, d->sa); + if(reply.length){ + send_reply(context, config, prependlength, d, &reply); + krb5_data_free(&reply); + } + if(ret) + kdc_log(context, config, 1, + "Failed processing %lu byte request from %s", + (unsigned long)len, d->addr_string); +} + +/* + * Handle incoming data to the UDP socket in `d' + */ + +static void +handle_udp(krb5_context context, + krb5_kdc_configuration *config, + struct descr *d) +{ + unsigned char *buf; + ssize_t n; + + buf = malloc(max_request_udp); + if (buf == NULL){ + kdc_log(context, config, 1, "Failed to allocate %lu bytes", + (unsigned long)max_request_udp); + return; + } + + d->sock_len = sizeof(d->__ss); + n = recvfrom(d->s, buf, max_request_udp, 0, d->sa, &d->sock_len); + if (rk_IS_SOCKET_ERROR(n)) { + if (rk_SOCK_ERRNO != EAGAIN && rk_SOCK_ERRNO != EINTR) + krb5_warn(context, rk_SOCK_ERRNO, "recvfrom"); + } else { + addr_to_string (context, d->sa, d->sock_len, + d->addr_string, sizeof(d->addr_string)); + if ((size_t)n == max_request_udp) { + krb5_data data; + krb5_warn(context, errno, + "recvfrom: truncated packet from %s, asking for TCP", + d->addr_string); + krb5_mk_error(context, + KRB5KRB_ERR_RESPONSE_TOO_BIG, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + &data); + send_reply(context, config, FALSE, d, &data); + krb5_data_free(&data); + } else { + do_request(context, config, buf, n, FALSE, d); + } + } + free (buf); +} + +static void +clear_descr(struct descr *d) +{ + if(d->buf) + memset(d->buf, 0, d->size); + d->len = 0; + if(d->s != rk_INVALID_SOCKET) + rk_closesocket(d->s); + d->s = rk_INVALID_SOCKET; +} + + +/* remove HTTP %-quoting from buf */ +static int +de_http(char *buf) +{ + unsigned char *p, *q; + unsigned int x; + + for (p = q = (unsigned char *)buf; *p; p++, q++) { + if (*p == '%') { + if (!(isxdigit(p[1]) && isxdigit(p[2]))) + return -1; + + if (sscanf((char *)p + 1, "%2x", &x) != 1) + return -1; + + *q = x; + p += 2; + } else { + *q = *p; + } + } + *q = '\0'; + return 0; +} + +#define TCP_TIMEOUT 4 + +/* + * accept a new TCP connection on `d[parent]' and store it in `d[child]' + */ + +static void +add_new_tcp (krb5_context context, + krb5_kdc_configuration *config, + struct descr *d, int parent, int child) +{ + krb5_socket_t s; + + if (child == -1) + return; + + d[child].sock_len = sizeof(d[child].__ss); + s = accept(d[parent].s, d[child].sa, &d[child].sock_len); + if(rk_IS_BAD_SOCKET(s)) { + if (rk_SOCK_ERRNO != EAGAIN && rk_SOCK_ERRNO != EINTR) + krb5_warn(context, rk_SOCK_ERRNO, "accept"); + return; + } + +#ifdef FD_SETSIZE + if (s >= FD_SETSIZE) { + krb5_warnx(context, "socket FD too large"); + rk_closesocket (s); + return; + } +#endif + + d[child].s = s; + d[child].timeout = time(NULL) + TCP_TIMEOUT; + d[child].type = SOCK_STREAM; + addr_to_string (context, + d[child].sa, d[child].sock_len, + d[child].addr_string, sizeof(d[child].addr_string)); +} + +/* + * Grow `d' to handle at least `n'. + * Return != 0 if fails + */ + +static int +grow_descr (krb5_context context, + krb5_kdc_configuration *config, + struct descr *d, size_t n) +{ + if (d->size - d->len < n) { + unsigned char *tmp; + size_t grow; + + grow = max(1024, d->len + n); + if (d->size + grow > max_request_tcp) { + kdc_log(context, config, 2, "Request exceeds max request size (%lu bytes).", + (unsigned long)d->size + grow); + clear_descr(d); + return -1; + } + tmp = realloc (d->buf, d->size + grow); + if (tmp == NULL) { + kdc_log(context, config, 1, "Failed to re-allocate %lu bytes.", + (unsigned long)d->size + grow); + clear_descr(d); + return -1; + } + d->size += grow; + d->buf = tmp; + } + return 0; +} + +/* + * Try to handle the TCP data at `d->buf, d->len'. + * Return -1 if failed, 0 if succesful, and 1 if data is complete. + */ + +static int +handle_vanilla_tcp (krb5_context context, + krb5_kdc_configuration *config, + struct descr *d) +{ + krb5_error_code ret; + krb5_storage *sp; + uint32_t len; + + if (d->len < 4) + return 0; + sp = krb5_storage_from_mem(d->buf, d->len); + if (sp == NULL) { + kdc_log (context, config, 1, "krb5_storage_from_mem failed"); + return -1; + } + ret = krb5_ret_uint32(sp, &len); + if (ret) { + kdc_log(context, config, 4, "failed to read request length"); + return -1; + } + krb5_storage_free(sp); + if(d->len - 4 >= len) { + memmove(d->buf, d->buf + 4, d->len - 4); + d->len -= 4; + return 1; + } + return 0; +} + +/* + * Try to handle the TCP/HTTP data at `d->buf, d->len'. + * Return -1 if failed, 0 if succesful, and 1 if data is complete. + */ + +static int +handle_http_tcp (krb5_context context, + krb5_kdc_configuration *config, + struct descr *d) +{ + char *s, *p, *t; + void *data; + char *proto; + int len; + + s = (char *)d->buf; + + /* If its a multi line query, truncate off the first line */ + p = strstr(s, "\r\n"); + if (p) + *p = 0; + + p = NULL; + t = strtok_r(s, " \t", &p); + if (t == NULL) { + kdc_log(context, config, 2, + "Missing HTTP operand (GET) request from %s", d->addr_string); + return -1; + } + + t = strtok_r(NULL, " \t", &p); + if(t == NULL) { + kdc_log(context, config, 2, + "Missing HTTP GET data in request from %s", d->addr_string); + return -1; + } + + data = malloc(strlen(t)); + if (data == NULL) { + kdc_log(context, config, 1, "Failed to allocate %lu bytes", + (unsigned long)strlen(t)); + return -1; + } + if(*t == '/') + t++; + if(de_http(t) != 0) { + kdc_log(context, config, 2, "Malformed HTTP request from %s", d->addr_string); + kdc_log(context, config, 4, "HTTP request: %s", t); + free(data); + return -1; + } + proto = strtok_r(NULL, " \t", &p); + if (proto == NULL) { + kdc_log(context, config, 2, "Malformed HTTP request from %s", d->addr_string); + free(data); + return -1; + } + len = rk_base64_decode(t, data); + if(len <= 0){ + const char *msg = + " 404 Not found\r\n" + "Server: Heimdal/" VERSION "\r\n" + "Cache-Control: no-cache\r\n" + "Pragma: no-cache\r\n" + "Content-type: text/html\r\n" + "Content-transfer-encoding: 8bit\r\n\r\n" + "<TITLE>404 Not found</TITLE>\r\n" + "<H1>404 Not found</H1>\r\n" + "That page doesn't exist, maybe you are looking for " + "<A HREF=\"http://www.h5l.org/\">Heimdal</A>?\r\n"; + kdc_log(context, config, 2, "HTTP request from %s is non KDC request", d->addr_string); + kdc_log(context, config, 4, "HTTP request: %s", t); + free(data); + if (rk_IS_SOCKET_ERROR(send(d->s, proto, strlen(proto), 0))) { + kdc_log(context, config, 1, "HTTP write failed: %s: %s", + d->addr_string, strerror(rk_SOCK_ERRNO)); + return -1; + } + if (rk_IS_SOCKET_ERROR(send(d->s, msg, strlen(msg), 0))) { + kdc_log(context, config, 1, "HTTP write failed: %s: %s", + d->addr_string, strerror(rk_SOCK_ERRNO)); + return -1; + } + return -1; + } + { + const char *msg = + " 200 OK\r\n" + "Server: Heimdal/" VERSION "\r\n" + "Cache-Control: no-cache\r\n" + "Pragma: no-cache\r\n" + "Content-type: application/octet-stream\r\n" + "Content-transfer-encoding: binary\r\n\r\n"; + if (rk_IS_SOCKET_ERROR(send(d->s, proto, strlen(proto), 0))) { + free(data); + kdc_log(context, config, 1, "HTTP write failed: %s: %s", + d->addr_string, strerror(rk_SOCK_ERRNO)); + return -1; + } + if (rk_IS_SOCKET_ERROR(send(d->s, msg, strlen(msg), 0))) { + free(data); + kdc_log(context, config, 1, "HTTP write failed: %s: %s", + d->addr_string, strerror(rk_SOCK_ERRNO)); + return -1; + } + } + if ((size_t)len > d->len) + len = d->len; + memcpy(d->buf, data, len); + d->len = len; + free(data); + return 1; +} + +static int +http1_request_taste(const unsigned char *req, size_t len) +{ + return !!((len >= sizeof("GET ") - 1 && + memcmp(req, "GET ", sizeof("GET ") - 1) == 0) || + (len >= sizeof("HEAD ") - 1 && + memcmp(req, "HEAD ", sizeof("HEAD ") - 1) == 0)); +} + +static int +http1_request_is_complete(const unsigned char *req, size_t len) +{ + + return http1_request_taste(req, len) && + memmem(req, len, "\r\n\r\n", sizeof("\r\n\r\n") - 4) != NULL; + + /* + * For POST (the MSFT variant of this protocol) we'll need something like + * this (plus check for Content-Length/Transfer-Encoding): + * + * const unsigned char *body; + * if ((body = memmem(req, len, "\r\n\r\n", sizeof("\r\n\r\n") - 4)) == NULL) + * return 0; + * body += sizeof("\r\n\r\n") - 4; + * len -= (body - req); + * return memmem(body, len, "\r\n\r\n", sizeof("\r\n\r\n") - 4) != NULL; + * + * Since the POST-based variant runs over HTTPS, we'll probably implement + * that in a proxy instead of here. + */ +} + +/* + * Handle incoming data to the TCP socket in `d[index]' + */ + +static void +handle_tcp(krb5_context context, + krb5_kdc_configuration *config, + struct descr *d, int idx, int min_free) +{ + unsigned char buf[1024]; + int n; + int ret = 0; + + if (d[idx].timeout == 0) { + add_new_tcp (context, config, d, idx, min_free); + return; + } + + n = recvfrom(d[idx].s, buf, sizeof(buf), 0, NULL, NULL); + if(rk_IS_SOCKET_ERROR(n)){ + krb5_warn(context, rk_SOCK_ERRNO, "recvfrom failed from %s to %s/%d", + d[idx].addr_string, descr_type(d + idx), + ntohs(d[idx].port)); + return; + } else if (n == 0) { + krb5_warnx(context, "connection closed before end of data after %lu " + "bytes from %s to %s/%d", (unsigned long)d[idx].len, + d[idx].addr_string, descr_type(d + idx), + ntohs(d[idx].port)); + clear_descr (d + idx); + return; + } + if (grow_descr (context, config, &d[idx], n)) + return; + memcpy(d[idx].buf + d[idx].len, buf, n); + d[idx].len += n; + if(d[idx].len > 4 && d[idx].buf[0] == 0) { + ret = handle_vanilla_tcp (context, config, &d[idx]); + } else if (enable_http && + http1_request_taste(d[idx].buf, d[idx].len)) { + + if (http1_request_is_complete(d[idx].buf, d[idx].len)) { + /* NUL-terminate at the request header ending \r\n\r\n */ + d[idx].buf[d[idx].len - 4] = '\0'; + ret = handle_http_tcp (context, config, &d[idx]); + } + } else if (d[idx].len > 4) { + kdc_log (context, config, + 2, "TCP data of strange type from %s to %s/%d", + d[idx].addr_string, descr_type(d + idx), + ntohs(d[idx].port)); + if (d[idx].buf[0] & 0x80) { + krb5_data reply; + + kdc_log (context, config, 2, "TCP extension not supported"); + + ret = krb5_mk_error(context, + KRB5KRB_ERR_FIELD_TOOLONG, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + &reply); + if (ret == 0) { + send_reply(context, config, TRUE, d + idx, &reply); + krb5_data_free(&reply); + } + } + clear_descr(d + idx); + return; + } + + /* + * ret == 0 -> not enough of request buffered -> wait for more + * ret == 1 -> go ahead and perform the request + * ret != 0 (really, < 0) -> error, probably ENOMEM, close connection + */ + if (ret == 1) + do_request(context, config, + d[idx].buf, d[idx].len, TRUE, &d[idx]); + + /* + * Note: this means we don't keep the connection open even where we + * the protocol permits it. + */ + if (ret != 0) + clear_descr(d + idx); +} + +#ifdef HAVE_FORK +static void +handle_islive(int fd) +{ + char buf; + int ret; + + ret = read(fd, &buf, 1); + if (ret != 1) + exit_flag = -1; +} +#endif + +static krb5_boolean +realloc_descrs(struct descr **d, unsigned int *ndescr) +{ + struct descr *tmp; + size_t i; + + tmp = realloc(*d, (*ndescr + 4) * sizeof(**d)); + if(tmp == NULL) + return FALSE; + + *d = tmp; + reinit_descrs (*d, *ndescr); + memset(*d + *ndescr, 0, 4 * sizeof(**d)); + for(i = *ndescr; i < *ndescr + 4; i++) + init_descr (*d + i); + + *ndescr += 4; + + return TRUE; +} + +static int +next_min_free(krb5_context context, struct descr **d, unsigned int *ndescr) +{ + size_t i; + int min_free; + + for(i = 0; i < *ndescr; i++) { + int s = (*d + i)->s; + if(rk_IS_BAD_SOCKET(s)) + return i; + } + + min_free = *ndescr; + if(!realloc_descrs(d, ndescr)) { + min_free = -1; + krb5_warnx(context, "No memory"); + } + + return min_free; +} + +static void +loop(krb5_context context, krb5_kdc_configuration *config, + struct descr **dp, unsigned int *ndescrp, int islive) +{ + struct descr *d = *dp; + unsigned int ndescr = *ndescrp; + + while (exit_flag == 0) { + struct timeval tmout; + fd_set fds; + int min_free = -1; + int max_fd = 0; + size_t i; + + FD_ZERO(&fds); + if (islive > -1) { + FD_SET(islive, &fds); + max_fd = islive; + } + for (i = 0; i < ndescr; i++) { + if (!rk_IS_BAD_SOCKET(d[i].s)) { + if (d[i].type == SOCK_STREAM && + d[i].timeout && d[i].timeout < time(NULL)) { + kdc_log(context, config, 2, + "TCP-connection from %s expired after %lu bytes", + d[i].addr_string, (unsigned long)d[i].len); + clear_descr(&d[i]); + continue; + } +#ifndef NO_LIMIT_FD_SETSIZE + if (max_fd < d[i].s) + max_fd = d[i].s; +#ifdef FD_SETSIZE + if (max_fd >= FD_SETSIZE) + krb5_errx(context, 1, "fd too large"); +#endif +#endif + FD_SET(d[i].s, &fds); + } + } + + tmout.tv_sec = TCP_TIMEOUT; + tmout.tv_usec = 0; + switch(select(max_fd + 1, &fds, 0, 0, &tmout)){ + case 0: + break; + case -1: + if (errno != EINTR) + krb5_warn(context, rk_SOCK_ERRNO, "select"); + break; + default: +#ifdef HAVE_FORK + if (islive > -1 && FD_ISSET(islive, &fds)) + handle_islive(islive); +#endif + for (i = 0; i < ndescr; i++) + if (!rk_IS_BAD_SOCKET(d[i].s) && FD_ISSET(d[i].s, &fds)) { + min_free = next_min_free(context, dp, ndescrp); + ndescr = *ndescrp; + d = *dp; + + if (d[i].type == SOCK_DGRAM) + handle_udp(context, config, &d[i]); + else if (d[i].type == SOCK_STREAM) + handle_tcp(context, config, d, i, min_free); + } + } + } + + switch (exit_flag) { + case -1: + kdc_log(context, config, 0, + "KDC worker process exiting because KDC master exited."); + break; +#ifdef SIGXCPU + case SIGXCPU: + kdc_log(context, config, 0, "CPU time limit exceeded"); + break; +#endif + case SIGINT: + case SIGTERM: + kdc_log(context, config, 0, "Terminated"); + break; + default: + kdc_log(context, config, 0, "Unexpected exit reason: %d", exit_flag); + break; + } +} + +#ifdef __APPLE__ +static void +bonjour_kid(krb5_context context, krb5_kdc_configuration *config, const char *argv0, int *islive) +{ + char buf; + + if (do_bonjour > 0) { + bonjour_announce(context, config); + + while (read(0, &buf, 1) == 1) + continue; + _exit(0); + } + + if ((bonjour_pid = fork()) != 0) + return; + + close(islive[0]); + if (dup2(islive[1], 0) == -1) + err(1, "failed to announce with bonjour (dup)"); + if (islive[1] != 0) + close(islive[1]); + execlp(argv0, "kdc", "--bonjour", NULL); + err(1, "failed to announce with bonjour (exec)"); +} +#endif + +#ifdef HAVE_FORK +static void +kill_kids(pid_t *pids, int max_kids, int sig) +{ + int i; + + for (i=0; i < max_kids; i++) + if (pids[i] > 0) + kill(sig, pids[i]); + if (bonjour_pid > 0) + kill(sig, bonjour_pid); +} + +static int +reap_kid(krb5_context context, krb5_kdc_configuration *config, + pid_t *pids, int max_kids, int options) +{ + pid_t pid; + char *what = "untracked"; + int status; + int i = 0; /* quiet warnings */ + int ret = 0; + int level = 3; + const char *sev = "info: "; + + pid = waitpid(-1, &status, options); + if (pid <= 0) + return 0; + + if (pid == bonjour_pid) { + bonjour_pid = (pid_t)-1; + what = "bonjour"; + } else { + for (i=0; i < max_kids; i++) { + if (pids[i] == pid) { + pids[i] = (pid_t)-1; + what = "worker"; + ret = 1; + break; + } + } + + if (i == max_kids) { + /* should not happen */ + sev = "warning: "; + level = 2; + } + } + + if (WIFEXITED(status)) + kdc_log(context, config, level, + "%sKDC reaped %s process: %d, exit status: %d", + sev, what, (int)pid, WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + kdc_log(context, config, level, + "%sKDC reaped %s process: %d, term signal %d%s", + sev, what, (int)pid, WTERMSIG(status), + WCOREDUMP(status) ? " (core dumped)" : ""); + else + kdc_log(context, config, level, "%sKDC reaped %s process: %d", + sev, what, (int)pid); + + return ret; +} + +static int +reap_kids(krb5_context context, krb5_kdc_configuration *config, + pid_t *pids, int max_kids) +{ + int reaped = 0; + + for (;;) { + if (reap_kid(context, config, pids, max_kids, WNOHANG) == 0) + break; + reaped++; + } + + return reaped; +} + +static void +select_sleep(int microseconds) +{ + struct timeval tv; + + tv.tv_sec = microseconds / 1000000; + tv.tv_usec = microseconds % 1000000; + select(0, NULL, NULL, NULL, &tv); +} +#endif + +void +start_kdc(krb5_context context, + krb5_kdc_configuration *config, const char *argv0) +{ + struct timeval tv1; + struct timeval tv2; + struct descr *d; + unsigned int ndescr; + pid_t pid = -1; +#ifdef HAVE_FORK + pid_t *pids; + int max_kdcs = config->num_kdc_processes; + int num_kdcs = 0; + int i; + int islive[2]; +#endif + +#ifdef __APPLE__ + if (!testing_flag && do_bonjour > 0) + bonjour_kid(context, config, argv0, NULL); +#endif + +#ifdef HAVE_FORK +#ifdef _SC_NPROCESSORS_ONLN + if (max_kdcs < 1) + max_kdcs = sysconf(_SC_NPROCESSORS_ONLN); +#endif + + if (max_kdcs < 1) + max_kdcs = 1; + + pids = calloc(max_kdcs, sizeof(*pids)); + if (pids == NULL) + krb5_err(context, 1, errno, "malloc"); + + /* + * We open a socketpair of which we hand one end to each of our kids. + * When we exit, for whatever reason, the children will notice an EOF + * on their end and be able to cleanly exit. + */ + + if (socketpair(PF_UNIX, SOCK_STREAM, 0, islive) == -1) + krb5_errx(context, 1, "socketpair"); + socket_set_nonblocking(islive[1], 1); +#endif + + ndescr = init_sockets(context, config, &d); + if(ndescr <= 0) + krb5_errx(context, 1, "No sockets!"); + +#ifdef HAVE_FORK + +# ifdef __APPLE__ + if (!testing_flag && do_bonjour < 0) + bonjour_kid(context, config, argv0, islive); +# endif + + kdc_log(context, config, 3, "KDC started master process pid=%d", getpid()); +#else + kdc_log(context, config, 3, "KDC started pid=%d", getpid()); +#endif + + roken_detach_finish(NULL, daemon_child); + +#ifdef HAVE_FORK + if (!testing_flag) { + /* Note that we might never execute the body of this loop */ + while (exit_flag == 0) { + + if (num_kdcs >= max_kdcs) { + num_kdcs -= reap_kid(context, config, pids, max_kdcs, 0); + continue; + } + + if (num_kdcs > 0) + num_kdcs -= reap_kids(context, config, pids, max_kdcs); + + pid = fork(); + switch (pid) { + case 0: + close(islive[0]); + loop(context, config, &d, &ndescr, islive[1]); + exit(0); + case -1: + /* XXXrcd: hmmm, do something useful?? */ + kdc_log(context, config, 1, + "KDC master process could not fork worker process"); + sleep(10); + break; + default: + for (i = 0; i < max_kdcs; i++) { + if (pids[i] <= 0) { + pids[i] = pid; + break; + } + } + if (i >= max_kdcs) { + /* This should not happen */ + kdc_log(context, config, 1, + "warning: forked untracked child process: %d", + (int)pid); + } + kdc_log(context, config, 3, "KDC worker process started: %d", + pid); + num_kdcs++; + /* Slow down the creation of KDCs... */ + select_sleep(12500); + break; + } + } + + /* Closing these sockets should cause the kids to die... */ + + close(islive[0]); + close(islive[1]); + + /* Close our listener sockets before terminating workers */ + for (i = 0; i < ndescr; ++i) + clear_descr(&d[i]); + + gettimeofday(&tv1, NULL); + tv2 = tv1; + + /* Reap every 10ms, terminate stragglers once a second, give up after 10 */ + for (;;) { + struct timeval tv3; + num_kdcs -= reap_kids(context, config, pids, max_kdcs); + if (num_kdcs == 0 && bonjour_pid <= 0) + goto end; + /* + * Using select to sleep will fail with EINTR if we receive a + * SIGCHLD. This is desirable. + */ + select_sleep(10000); + gettimeofday(&tv3, NULL); + if (tv3.tv_sec - tv1.tv_sec > 10 || + (tv3.tv_sec - tv1.tv_sec == 10 && tv3.tv_usec >= tv1.tv_usec)) + break; + if (tv3.tv_sec - tv2.tv_sec > 1 || + (tv3.tv_sec - tv2.tv_sec == 1 && tv3.tv_usec >= tv2.tv_usec)) { + kill_kids(pids, max_kdcs, SIGTERM); + tv2 = tv3; + } + } + + /* Kill stragglers and reap every 200ms, give up after 15s */ + for (;;) { + kill_kids(pids, max_kdcs, SIGKILL); + num_kdcs -= reap_kids(context, config, pids, max_kdcs); + if (num_kdcs == 0 && bonjour_pid <= 0) + break; + select_sleep(200000); + gettimeofday(&tv2, NULL); + if (tv2.tv_sec - tv1.tv_sec > 15 || + (tv2.tv_sec - tv1.tv_sec == 15 && tv2.tv_usec >= tv1.tv_usec)) + break; + } + + end: + kdc_log(context, config, 3, "KDC master process exiting"); + } else { + loop(context, config, &d, &ndescr, -1); + kdc_log(context, config, 3, "KDC exiting"); + } + free(pids); +#else + loop(context, config, &d, &ndescr, -1); + kdc_log(context, config, 3, "KDC exiting"); +#endif + + free(d); +} diff --git a/third_party/heimdal/kdc/csr_authorizer.c b/third_party/heimdal/kdc/csr_authorizer.c new file mode 100644 index 0000000..52bc37c --- /dev/null +++ b/third_party/heimdal/kdc/csr_authorizer.c @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019 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. + */ + +#include "kdc_locl.h" +#include "csr_authorizer_plugin.h" + +struct plctx { + const char *app; + hx509_request csr; + krb5_const_principal client; + krb5_boolean result; +}; + +static krb5_error_code KRB5_LIB_CALL +plcallback(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + const krb5plugin_csr_authorizer_ftable *authorizer = plug; + struct plctx *plctx = userctx; + + return authorizer->authorize(plugctx, context, plctx->app, plctx->csr, + plctx->client, &plctx->result); +} + +static const char *plugin_deps[] = { "krb5", NULL }; + +static struct heim_plugin_data csr_authorizer_data = { + "kdc", + KDC_CSR_AUTHORIZER, + 1, + plugin_deps, + krb5_get_instance +}; + +/* + * Invoke a plugin to validate a JWT/SAML/OIDC token and partially-evaluate + * access control. + */ +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +kdc_authorize_csr(krb5_context context, + const char *app, + hx509_request csr, + krb5_const_principal client) +{ + krb5_error_code ret; + struct plctx ctx; + + ctx.app = app; + ctx.csr = csr; + ctx.client = client; + ctx.result = FALSE; + + ret = _krb5_plugin_run_f(context, &csr_authorizer_data, 0, &ctx, + plcallback); + if (ret) + krb5_prepend_error_message(context, ret, "Authorization of requested " + "certificate extensions failed"); + else if (!ctx.result) + krb5_set_error_message(context, ret, "Authorization of requested " + "certificate extensions failed"); + return ret; +} diff --git a/third_party/heimdal/kdc/csr_authorizer_plugin.h b/third_party/heimdal/kdc/csr_authorizer_plugin.h new file mode 100644 index 0000000..022feda --- /dev/null +++ b/third_party/heimdal/kdc/csr_authorizer_plugin.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019 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. + */ + +#ifndef HEIMDAL_KDC_CSR_AUTHORIZER_PLUGIN_H +#define HEIMDAL_KDC_CSR_AUTHORIZER_PLUGIN_H 1 + +#define KDC_CSR_AUTHORIZER "kdc_csr_authorizer" +#define KDC_CSR_AUTHORIZER_VERSION_0 0 + +/* + * @param init Plugin initialization function (see krb5-plugin(7)) + * @param minor_version The plugin minor version number (0) + * @param fini Plugin finalization function + * @param authorize Plugin CSR authorization function + * + * The authorize field is the plugin entry point that performs authorization of + * CSRs for kx509 however the plugin desires. It is invoked in no particular + * order relative to other CSR authorization plugins. The plugin authorize + * function must return KRB5_PLUGIN_NO_HANDLE if the rule is not applicable to + * it. + * + * The plugin authorize function has the following arguments, in this + * order: + * + * -# plug_ctx, the context value output by the plugin's init function + * -# context, a krb5_context + * -# app, the name of the application + * -# csr, a hx509_request + * -# client, a krb5_const_principal + * -# authorization_result, a pointer to a krb5_boolean + * + * @ingroup krb5_support + */ +typedef struct krb5plugin_csr_authorizer_ftable_desc { + HEIM_PLUGIN_FTABLE_COMMON_ELEMENTS(krb5_context); + krb5_error_code (KRB5_LIB_CALL *authorize)(void *, /*plug_ctx*/ + krb5_context, /*context*/ + const char *, /*app*/ + hx509_request, /*CSR*/ + krb5_const_principal,/*client*/ + krb5_boolean *); /*authorized*/ +} krb5plugin_csr_authorizer_ftable; + +#endif /* HEIMDAL_KDC_CSR_AUTHORIZER_PLUGIN_H */ diff --git a/third_party/heimdal/kdc/default_config.c b/third_party/heimdal/kdc/default_config.c new file mode 100644 index 0000000..8301b90 --- /dev/null +++ b/third_party/heimdal/kdc/default_config.c @@ -0,0 +1,464 @@ +/* + * Copyright (c) 1997-2007 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2009 Apple Inc. 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. + */ + +#include "kdc_locl.h" +#include <getarg.h> +#include <parse_bytes.h> + +static const char *sysplugin_dirs[] = { +#ifdef _WIN32 + "$ORIGIN", +#else + "$ORIGIN/../lib/plugin/kdc", +#endif +#ifdef __APPLE__ + LIBDIR "/plugin/kdc", +#endif + NULL +}; + +static void +load_kdc_plugins_once(void *ctx) +{ + krb5_context context = ctx; + 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 + + _krb5_load_plugins(context, "kdc", (const char **)dirs); + +#ifndef _WIN32 + krb5_config_free_strings(cfdirs); +#endif +} + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config) +{ + static heim_base_once_t load_kdc_plugins = HEIM_BASE_ONCE_INIT; + krb5_kdc_configuration *c; + krb5_error_code ret; + + heim_base_once_f(&load_kdc_plugins, context, load_kdc_plugins_once); + + c = calloc(1, sizeof(*c)); + if (c == NULL) { + krb5_set_error_message(context, ENOMEM, "malloc: out of memory"); + return ENOMEM; + } + + c->app = "kdc"; + c->num_kdc_processes = -1; + c->require_preauth = TRUE; + c->kdc_warn_pwexpire = 0; + c->encode_as_rep_as_tgs_rep = FALSE; + c->tgt_use_strongest_session_key = FALSE; + c->preauth_use_strongest_session_key = FALSE; + c->svc_use_strongest_session_key = FALSE; + c->use_strongest_server_key = TRUE; + c->check_ticket_addresses = TRUE; + c->warn_ticket_addresses = FALSE; + c->allow_null_ticket_addresses = TRUE; + c->allow_anonymous = FALSE; + c->historical_anon_realm = FALSE; + c->strict_nametypes = FALSE; + c->trpolicy = TRPOLICY_ALWAYS_CHECK; + c->require_pac = FALSE; + c->disable_pac = FALSE; + c->enable_fast = TRUE; + c->enable_fast_cookie = TRUE; + c->enable_armored_pa_enc_timestamp = TRUE; + c->enable_unarmored_pa_enc_timestamp = TRUE; + c->enable_pkinit = FALSE; + c->require_pkinit_freshness = FALSE; + c->pkinit_princ_in_cert = TRUE; + c->pkinit_require_binding = TRUE; + c->synthetic_clients = FALSE; + c->pkinit_max_life_from_cert_extension = FALSE; + c->pkinit_max_life_bound = 0; + c->synthetic_clients_max_life = 300; + c->synthetic_clients_max_renew = 300; + c->pkinit_dh_min_bits = 1024; + c->db = NULL; + c->num_db = 0; + c->logf = NULL; + + c->num_kdc_processes = + krb5_config_get_int_default(context, NULL, c->num_kdc_processes, + "kdc", "num-kdc-processes", NULL); + + c->require_preauth = + krb5_config_get_bool_default(context, NULL, + c->require_preauth, + "kdc", "require-preauth", NULL); +#ifdef DIGEST + c->enable_digest = + krb5_config_get_bool_default(context, NULL, + FALSE, + "kdc", "enable-digest", NULL); + + { + const char *digests; + + digests = krb5_config_get_string(context, NULL, + "kdc", + "digests_allowed", NULL); + if (digests == NULL) + digests = "ntlm-v2"; + c->digests_allowed = parse_flags(digests,_kdc_digestunits, 0); + if (c->digests_allowed == -1) { + kdc_log(context, c, 0, + "unparsable digest units (%s), turning off digest", + digests); + c->enable_digest = 0; + } else if (c->digests_allowed == 0) { + kdc_log(context, c, 0, "no digest enable, turning digest off"); + c->enable_digest = 0; + } + } +#endif + +#ifdef KX509 + c->enable_kx509 = + krb5_config_get_bool_default(context, NULL, + FALSE, + "kdc", "enable_kx509", NULL); +#endif + + c->tgt_use_strongest_session_key = + krb5_config_get_bool_default(context, NULL, + c->tgt_use_strongest_session_key, + "kdc", + "tgt-use-strongest-session-key", NULL); + c->preauth_use_strongest_session_key = + krb5_config_get_bool_default(context, NULL, + c->preauth_use_strongest_session_key, + "kdc", + "preauth-use-strongest-session-key", NULL); + c->svc_use_strongest_session_key = + krb5_config_get_bool_default(context, NULL, + c->svc_use_strongest_session_key, + "kdc", + "svc-use-strongest-session-key", NULL); + c->use_strongest_server_key = + krb5_config_get_bool_default(context, NULL, + c->use_strongest_server_key, + "kdc", + "use-strongest-server-key", NULL); + + c->check_ticket_addresses = + krb5_config_get_bool_default(context, NULL, + c->check_ticket_addresses, + "kdc", + "check-ticket-addresses", NULL); + c->warn_ticket_addresses = + krb5_config_get_bool_default(context, NULL, + c->warn_ticket_addresses, + "kdc", + "warn_ticket_addresses", NULL); + c->allow_null_ticket_addresses = + krb5_config_get_bool_default(context, NULL, + c->allow_null_ticket_addresses, + "kdc", + "allow-null-ticket-addresses", NULL); + + c->allow_anonymous = + krb5_config_get_bool_default(context, NULL, + c->allow_anonymous, + "kdc", + "allow-anonymous", NULL); + + c->historical_anon_realm = + krb5_config_get_bool_default(context, NULL, + c->historical_anon_realm, + "kdc", + "historical_anon_realm", NULL); + + c->strict_nametypes = + krb5_config_get_bool_default(context, NULL, + c->strict_nametypes, + "kdc", + "strict-nametypes", NULL); + + c->max_datagram_reply_length = + krb5_config_get_int_default(context, + NULL, + 1400, + "kdc", + "max-kdc-datagram-reply-length", + NULL); + + { + const char *trpolicy_str; + + trpolicy_str = + krb5_config_get_string_default(context, NULL, "DEFAULT", "kdc", + "transited-policy", NULL); + if(strcasecmp(trpolicy_str, "always-check") == 0) { + c->trpolicy = TRPOLICY_ALWAYS_CHECK; + } else if(strcasecmp(trpolicy_str, "allow-per-principal") == 0) { + c->trpolicy = TRPOLICY_ALLOW_PER_PRINCIPAL; + } else if(strcasecmp(trpolicy_str, "always-honour-request") == 0) { + c->trpolicy = TRPOLICY_ALWAYS_HONOUR_REQUEST; + } else if(strcasecmp(trpolicy_str, "DEFAULT") == 0) { + /* default */ + } else { + kdc_log(context, c, 0, + "unknown transited-policy: %s, " + "reverting to default (always-check)", + trpolicy_str); + } + } + + c->encode_as_rep_as_tgs_rep = + krb5_config_get_bool_default(context, NULL, + c->encode_as_rep_as_tgs_rep, + "kdc", + "encode_as_rep_as_tgs_rep", NULL); + + c->kdc_warn_pwexpire = + krb5_config_get_time_default (context, NULL, + c->kdc_warn_pwexpire, + "kdc", "kdc_warn_pwexpire", NULL); + + c->require_pac = + krb5_config_get_bool_default(context, + NULL, + c->require_pac, + "kdc", + "require_pac", + NULL); + + c->disable_pac = + krb5_config_get_bool_default(context, + NULL, + c->disable_pac, + "kdc", + "disable_pac", + NULL); + + c->enable_fast = + krb5_config_get_bool_default(context, + NULL, + c->enable_fast, + "kdc", + "enable_fast", + NULL); + + c->enable_fast_cookie = + krb5_config_get_bool_default(context, + NULL, + c->enable_fast_cookie, + "kdc", + "enable_fast_cookie", + NULL); + + c->enable_armored_pa_enc_timestamp = + krb5_config_get_bool_default(context, + NULL, + c->enable_armored_pa_enc_timestamp, + "kdc", + "enable_armored_pa_enc_timestamp", + NULL); + + c->enable_unarmored_pa_enc_timestamp = + krb5_config_get_bool_default(context, + NULL, + c->enable_unarmored_pa_enc_timestamp, + "kdc", + "enable_unarmored_pa_enc_timestamp", + NULL); + + c->enable_pkinit = + krb5_config_get_bool_default(context, + NULL, + c->enable_pkinit, + "kdc", + "enable-pkinit", + NULL); + + c->require_pkinit_freshness = + krb5_config_get_bool_default(context, + NULL, + c->require_pkinit_freshness, + "kdc", + "require-pkinit-freshness", + NULL); + + c->pkinit_kdc_identity = + krb5_config_get_string(context, NULL, + "kdc", "pkinit_identity", NULL); + c->pkinit_kdc_anchors = + krb5_config_get_string(context, NULL, + "kdc", "pkinit_anchors", NULL); + c->pkinit_kdc_cert_pool = + krb5_config_get_strings(context, NULL, + "kdc", "pkinit_pool", NULL); + c->pkinit_kdc_revoke = + krb5_config_get_strings(context, NULL, + "kdc", "pkinit_revoke", NULL); + c->pkinit_kdc_ocsp_file = + krb5_config_get_string(context, NULL, + "kdc", "pkinit_kdc_ocsp", NULL); + c->pkinit_kdc_friendly_name = + krb5_config_get_string(context, NULL, + "kdc", "pkinit_kdc_friendly_name", NULL); + c->pkinit_princ_in_cert = + krb5_config_get_bool_default(context, NULL, + c->pkinit_princ_in_cert, + "kdc", + "pkinit_principal_in_certificate", + NULL); + c->pkinit_require_binding = + krb5_config_get_bool_default(context, NULL, + c->pkinit_require_binding, + "kdc", + "pkinit_win2k_require_binding", + NULL); + c->pkinit_dh_min_bits = + krb5_config_get_int_default(context, NULL, + 0, + "kdc", "pkinit_dh_min_bits", NULL); + + c->pkinit_max_life_from_cert_extension = + krb5_config_get_bool_default(context, NULL, + c->pkinit_max_life_from_cert_extension, + "kdc", + "pkinit_max_life_from_cert_extension", + NULL); + + c->synthetic_clients = + krb5_config_get_bool_default(context, NULL, + c->synthetic_clients, + "kdc", + "synthetic_clients", + NULL); + + c->pkinit_max_life_bound = + krb5_config_get_time_default(context, NULL, 0, "kdc", + "pkinit_max_life_bound", + NULL); + + c->pkinit_max_life_from_cert = + krb5_config_get_time_default(context, NULL, 0, "kdc", + "pkinit_max_life_from_cert", + NULL); + + c->synthetic_clients_max_life = + krb5_config_get_time_default(context, NULL, 300, "kdc", + "synthetic_clients_max_life", + NULL); + + c->synthetic_clients_max_renew = + krb5_config_get_time_default(context, NULL, 300, "kdc", + "synthetic_clients_max_renew", + NULL); + + c->enable_gss_preauth = + krb5_config_get_bool_default(context, NULL, + c->enable_gss_preauth, + "kdc", + "enable_gss_preauth", NULL); + + c->enable_gss_auth_data = + krb5_config_get_bool_default(context, NULL, + c->enable_gss_auth_data, + "kdc", + "enable_gss_auth_data", NULL); + + ret = _kdc_gss_get_mechanism_config(context, "kdc", + "gss_mechanisms_allowed", + &c->gss_mechanisms_allowed); + if (ret) { + free(c); + return ret; + } + + ret = _kdc_gss_get_mechanism_config(context, "kdc", + "gss_cross_realm_mechanisms_allowed", + &c->gss_cross_realm_mechanisms_allowed); + if (ret) { + OM_uint32 minor; + gss_release_oid_set(&minor, &c->gss_mechanisms_allowed); + free(c); + return ret; + } + + *config = c; + + return 0; +} + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +krb5_kdc_pkinit_config(krb5_context context, krb5_kdc_configuration *config) +{ +#ifdef PKINIT +#ifdef __APPLE__ + config->enable_pkinit = 1; + + if (config->pkinit_kdc_identity == NULL) { + if (config->pkinit_kdc_friendly_name == NULL) + config->pkinit_kdc_friendly_name = + strdup("O=System Identity,CN=com.apple.kerberos.kdc"); + config->pkinit_kdc_identity = strdup("KEYCHAIN:"); + } + if (config->pkinit_kdc_anchors == NULL) + config->pkinit_kdc_anchors = strdup("KEYCHAIN:"); + +#endif /* __APPLE__ */ + + if (config->enable_pkinit) { + if (config->pkinit_kdc_identity == NULL) + krb5_errx(context, 1, "pkinit enabled but no identity"); + + if (config->pkinit_kdc_anchors == NULL) + krb5_errx(context, 1, "pkinit enabled but no X509 anchors"); + + krb5_kdc_pk_initialize(context, config, + config->pkinit_kdc_identity, + config->pkinit_kdc_anchors, + config->pkinit_kdc_cert_pool, + config->pkinit_kdc_revoke); + + } + + return 0; +#endif /* PKINIT */ +} diff --git a/third_party/heimdal/kdc/digest-service.c b/third_party/heimdal/kdc/digest-service.c new file mode 100644 index 0000000..275efaf --- /dev/null +++ b/third_party/heimdal/kdc/digest-service.c @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2006 - 2007 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2009 Apple Inc. 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 HC_DEPRECATED_CRYPTO + +#include "headers.h" +#include <digest_asn1.h> +#include <heimntlm.h> +#include <heim-ipc.h> +#include <getarg.h> + +typedef struct pk_client_params pk_client_params; +typedef struct gss_client_params gss_client_params; +struct DigestREQ; +struct Kx509Request; + +#include <kdc-private.h> + +krb5_kdc_configuration *config; + +static void +ntlm_service(void *ctx, const heim_idata *req, + const heim_icred cred, + heim_ipc_complete complete, + heim_sipc_call cctx) +{ + NTLMRequest2 ntq; + unsigned char sessionkey[16]; + heim_idata rep = { 0, NULL }; + krb5_context context = ctx; + hdb_entry *user = NULL; + HDB *db = NULL; + Key *key = NULL; + NTLMReply ntp; + size_t size; + int ret; + const char *domain; + + kdc_log(context, config, 4, "digest-request: uid=%d", + (int)heim_ipc_cred_get_uid(cred)); + + if (heim_ipc_cred_get_uid(cred) != 0) { + (*complete)(cctx, EPERM, NULL); + return; + } + + ntp.success = 0; + ntp.flags = 0; + ntp.sessionkey = NULL; + + ret = decode_NTLMRequest2(req->data, req->length, &ntq, NULL); + if (ret) + goto failed; + + /* XXX forward to NetrLogonSamLogonEx() if not a local domain */ + if (strcmp(ntq.loginDomainName, "BUILTIN") == 0) { + domain = ntq.loginDomainName; + } else if (strcmp(ntq.loginDomainName, "") == 0) { + domain = "BUILTIN"; + } else { + ret = EINVAL; + goto failed; + } + + kdc_log(context, config, 4, "digest-request: user=%s/%s", + ntq.loginUserName, domain); + + if (ntq.lmchallenge.length != 8) + goto failed; + + if (ntq.ntChallengeResponce.length == 0) + goto failed; + + { + krb5_principal client; + + ret = krb5_make_principal(context, &client, domain, + ntq.loginUserName, NULL); + if (ret) + goto failed; + + krb5_principal_set_type(context, client, KRB5_NT_NTLM); + + ret = _kdc_db_fetch(context, config, client, + HDB_F_GET_CLIENT, NULL, &db, &user); + krb5_free_principal(context, client); + if (ret) + goto failed; + + ret = hdb_enctype2key(context, user, NULL, + ETYPE_ARCFOUR_HMAC_MD5, &key); + if (ret) { + krb5_set_error_message(context, ret, "NTLM missing arcfour key"); + goto failed; + } + } + + kdc_log(context, config, 5, + "digest-request: found user, processing ntlm request"); + + if (ntq.ntChallengeResponce.length != 24) { + struct ntlm_buf infotarget, answer; + + answer.length = ntq.ntChallengeResponce.length; + answer.data = ntq.ntChallengeResponce.data; + + ret = heim_ntlm_verify_ntlm2(key->key.keyvalue.data, + key->key.keyvalue.length, + ntq.loginUserName, + ntq.loginDomainName, + 0, + ntq.lmchallenge.data, + &answer, + &infotarget, + sessionkey); + if (ret) { + goto failed; + } + + free(infotarget.data); + /* XXX verify info target */ + + } else { + struct ntlm_buf answer; + + if (ntq.flags & NTLM_NEG_NTLM2_SESSION) { + unsigned char sessionhash[MD5_DIGEST_LENGTH]; + EVP_MD_CTX *md5ctx; + + /* the first first 8 bytes is the challenge, what is the other 16 bytes ? */ + if (ntq.lmChallengeResponce.length != 24) + goto failed; + + md5ctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(md5ctx, EVP_md5(), NULL); + EVP_DigestUpdate(md5ctx, ntq.lmchallenge.data, 8); + EVP_DigestUpdate(md5ctx, ntq.lmChallengeResponce.data, 8); + EVP_DigestFinal_ex(md5ctx, sessionhash, NULL); + EVP_MD_CTX_destroy(md5ctx); + memcpy(ntq.lmchallenge.data, sessionhash, ntq.lmchallenge.length); + } + + ret = heim_ntlm_calculate_ntlm1(key->key.keyvalue.data, + key->key.keyvalue.length, + ntq.lmchallenge.data, &answer); + if (ret) + goto failed; + + if (ntq.ntChallengeResponce.length != answer.length || + ct_memcmp(ntq.ntChallengeResponce.data, answer.data, answer.length) != 0) { + free(answer.data); + ret = EINVAL; + goto failed; + } + free(answer.data); + + { + EVP_MD_CTX *ctxp; + + ctxp = EVP_MD_CTX_create(); + EVP_DigestInit_ex(ctxp, EVP_md4(), NULL); + EVP_DigestUpdate(ctxp, key->key.keyvalue.data, key->key.keyvalue.length); + EVP_DigestFinal_ex(ctxp, sessionkey, NULL); + EVP_MD_CTX_destroy(ctxp); + } + } + + ntp.success = 1; + + ASN1_MALLOC_ENCODE(NTLMReply, rep.data, rep.length, &ntp, &size, ret); + if (ret) + goto failed; + if (rep.length != size) + abort(); + + failed: + kdc_log(context, config, 4, "digest-request: %d", ret); + + (*complete)(cctx, ret, &rep); + + free(rep.data); + + free_NTLMRequest2(&ntq); + if (user) + _kdc_free_ent (context, db, user); +} + +static int help_flag; +static int version_flag; + +static struct getargs args[] = { + { "help", 'h', arg_flag, &help_flag, NULL, NULL }, + { "version", 'v', arg_flag, &version_flag, NULL, NULL } +}; + +static int num_args = sizeof(args) / sizeof(args[0]); + +static void +usage(int ret) +{ + arg_printusage (args, num_args, NULL, ""); + exit (ret); +} + +int +main(int argc, char **argv) +{ + krb5_context context; + int ret, optidx = 0; + + setprogname(argv[0]); + + if (getarg(args, num_args, argc, argv, &optidx)) + usage(1); + + if (help_flag) + usage(0); + + if (version_flag) { + print_version(NULL); + exit(0); + } + + ret = krb5_init_context(&context); + if (ret) + krb5_errx(context, 1, "krb5_init_context"); + + ret = krb5_kdc_get_config(context, &config); + if (ret) + krb5_err(context, 1, ret, "krb5_kdc_default_config"); + + kdc_openlog(context, "digest-service", config); + + ret = krb5_kdc_set_dbinfo(context, config); + if (ret) + krb5_err(context, 1, ret, "krb5_kdc_set_dbinfo"); + +#if __APPLE__ + { + heim_sipc mach; + heim_sipc_launchd_mach_init("org.h5l.ntlm-service", + ntlm_service, context, &mach); + heim_sipc_timeout(60); + } +#endif +#ifdef HAVE_DOOR_CREATE + { + heim_sipc door; + heim_sipc_service_door("org.h5l.ntlm-service", ntlm_service, NULL, &door); + } +#endif + { + heim_sipc un; + heim_sipc_service_unix("org.h5l.ntlm-service", ntlm_service, NULL, &un); + } + + heim_ipc_main(); + return 0; +} diff --git a/third_party/heimdal/kdc/digest.c b/third_party/heimdal/kdc/digest.c new file mode 100644 index 0000000..3285aaa --- /dev/null +++ b/third_party/heimdal/kdc/digest.c @@ -0,0 +1,1520 @@ +/* + * Copyright (c) 2006 - 2007 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. + */ + +#include "kdc_locl.h" +#include <hex.h> + +#ifdef DIGEST + +#define MS_CHAP_V2 0x20 +#define CHAP_MD5 0x10 +#define DIGEST_MD5 0x08 +#define NTLM_V2 0x04 +#define NTLM_V1_SESSION 0x02 +#define NTLM_V1 0x01 + +const struct units _kdc_digestunits[] = { + {"ms-chap-v2", 1U << 5}, + {"chap-md5", 1U << 4}, + {"digest-md5", 1U << 3}, + {"ntlm-v2", 1U << 2}, + {"ntlm-v1-session", 1U << 1}, + {"ntlm-v1", 1U << 0}, + {NULL, 0} +}; + + +static krb5_error_code +get_digest_key(krb5_context context, + krb5_kdc_configuration *config, + hdb_entry *server, + krb5_crypto *crypto) +{ + krb5_error_code ret; + krb5_enctype enctype; + Key *key; + + ret = _kdc_get_preferred_key(context, + config, + server, + "digest-service", + &enctype, + &key); + if (ret) + return ret; + return krb5_crypto_init(context, &key->key, 0, crypto); +} + +/* + * + */ + +static char * +get_ntlm_targetname(krb5_context context, + hdb_entry *client) +{ + char *targetname, *p; + + targetname = strdup(krb5_principal_get_realm(context, + client->principal)); + if (targetname == NULL) + return NULL; + + p = strchr(targetname, '.'); + if (p) + *p = '\0'; + + strupr(targetname); + return targetname; +} + +static krb5_error_code +fill_targetinfo(krb5_context context, + char *targetname, + hdb_entry *client, + krb5_data *data) +{ + struct ntlm_targetinfo ti; + krb5_error_code ret; + struct ntlm_buf d; + krb5_principal p; + const char *str; + + memset(&ti, 0, sizeof(ti)); + + ti.domainname = targetname; + p = client->principal; + str = krb5_principal_get_comp_string(context, p, 0); + if (str != NULL && + (strcmp("host", str) == 0 || + strcmp("ftp", str) == 0 || + strcmp("imap", str) == 0 || + strcmp("pop", str) == 0 || + strcmp("smtp", str))) + { + str = krb5_principal_get_comp_string(context, p, 1); + ti.dnsservername = rk_UNCONST(str); + } + + ret = heim_ntlm_encode_targetinfo(&ti, 1, &d); + if (ret) + return ret; + + data->data = d.data; + data->length = d.length; + + return 0; +} + + +static const unsigned char ms_chap_v2_magic1[39] = { + 0x4D, 0x61, 0x67, 0x69, 0x63, 0x20, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x20, 0x74, 0x6F, 0x20, 0x63, 0x6C, 0x69, 0x65, + 0x6E, 0x74, 0x20, 0x73, 0x69, 0x67, 0x6E, 0x69, 0x6E, 0x67, + 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x74 +}; +static const unsigned char ms_chap_v2_magic2[41] = { + 0x50, 0x61, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x6D, 0x61, 0x6B, + 0x65, 0x20, 0x69, 0x74, 0x20, 0x64, 0x6F, 0x20, 0x6D, 0x6F, + 0x72, 0x65, 0x20, 0x74, 0x68, 0x61, 0x6E, 0x20, 0x6F, 0x6E, + 0x65, 0x20, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, + 0x6E +}; +static const unsigned char ms_rfc3079_magic1[27] = { + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x4d, 0x50, 0x50, 0x45, 0x20, 0x4d, + 0x61, 0x73, 0x74, 0x65, 0x72, 0x20, 0x4b, 0x65, 0x79 +}; + +/* + * + */ + +static krb5_error_code +get_password_entry(krb5_context context, + krb5_kdc_configuration *config, + const char *username, + char **password) +{ + krb5_principal clientprincipal; + krb5_error_code ret; + hdb_entry *user; + HDB *db; + + /* get username */ + ret = krb5_parse_name(context, username, &clientprincipal); + if (ret) + return ret; + + ret = _kdc_db_fetch(context, config, clientprincipal, + HDB_F_GET_CLIENT, NULL, &db, &user); + krb5_free_principal(context, clientprincipal); + if (ret) + return ret; + + ret = hdb_entry_get_password(context, db, user, password); + if (ret || password == NULL) { + if (ret == 0) { + ret = EINVAL; + krb5_set_error_message(context, ret, "password missing"); + } + memset(user, 0, sizeof(*user)); + } + _kdc_free_ent (context, db, user); + return ret; +} + +/* + * + */ + +krb5_error_code +_kdc_do_digest(krb5_context context, + krb5_kdc_configuration *config, + const struct DigestREQ *req, krb5_data *reply, + const char *from, struct sockaddr *addr) +{ + krb5_error_code ret = 0; + krb5_ticket *ticket = NULL; + krb5_auth_context ac = NULL; + krb5_keytab id = NULL; + krb5_crypto crypto = NULL; + DigestReqInner ireq; + DigestRepInner r; + DigestREP rep; + krb5_flags ap_req_options; + krb5_data buf; + size_t size; + krb5_storage *sp = NULL; + Checksum res; + HDB *serverdb, *userdb; + hdb_entry *server = NULL, *user = NULL; + HDB *clientdb; + hdb_entry *client = NULL; + char *client_name = NULL, *password = NULL; + krb5_data serverNonce; + + if(!config->enable_digest) { + kdc_log(context, config, 2, + "Rejected digest request (disabled) from %s", from); + return KRB5KDC_ERR_POLICY; + } + + krb5_data_zero(&buf); + krb5_data_zero(reply); + krb5_data_zero(&serverNonce); + memset(&ireq, 0, sizeof(ireq)); + memset(&r, 0, sizeof(r)); + memset(&rep, 0, sizeof(rep)); + memset(&res, 0, sizeof(res)); + + kdc_log(context, config, 3, "Digest request from %s", from); + + ret = krb5_kt_resolve(context, "HDBGET:", &id); + if (ret) { + kdc_log(context, config, 0, "Can't open database for digest"); + goto out; + } + + ret = krb5_rd_req(context, + &ac, + &req->apReq, + NULL, + id, + &ap_req_options, + &ticket); + if (ret) + goto out; + + /* check the server principal in the ticket matches digest/R@R */ + { + krb5_principal principal = NULL; + const char *p, *rr; + + ret = krb5_ticket_get_server(context, ticket, &principal); + if (ret) + goto out; + + ret = EINVAL; + krb5_set_error_message(context, ret, "Wrong digest server principal used"); + p = krb5_principal_get_comp_string(context, principal, 0); + if (p == NULL) { + krb5_free_principal(context, principal); + goto out; + } + if (strcmp(p, KRB5_DIGEST_NAME) != 0) { + krb5_free_principal(context, principal); + goto out; + } + + p = krb5_principal_get_comp_string(context, principal, 1); + if (p == NULL) { + krb5_free_principal(context, principal); + goto out; + } + rr = krb5_principal_get_realm(context, principal); + if (rr == NULL) { + krb5_free_principal(context, principal); + goto out; + } + if (strcmp(p, rr) != 0) { + krb5_free_principal(context, principal); + goto out; + } + krb5_clear_error_message(context); + + ret = _kdc_db_fetch(context, config, principal, + HDB_F_GET_SERVER, NULL, &serverdb, &server); + if (ret) + goto out; + + krb5_free_principal(context, principal); + } + + /* check the client is allowed to do digest auth */ + { + krb5_principal principal = NULL; + + ret = krb5_ticket_get_client(context, ticket, &principal); + if (ret) + goto out; + + ret = krb5_unparse_name(context, principal, &client_name); + if (ret) { + krb5_free_principal(context, principal); + goto out; + } + + ret = _kdc_db_fetch(context, config, principal, + HDB_F_GET_CLIENT, NULL, &clientdb, &client); + krb5_free_principal(context, principal); + if (ret) + goto out; + + if (client->flags.allow_digest == 0) { + kdc_log(context, config, 2, + "Client %s tried to use digest " + "but is not allowed to", + client_name); + ret = KRB5KDC_ERR_POLICY; + krb5_set_error_message(context, ret, + "Client is not permitted to use digest"); + goto out; + } + } + + /* unpack request */ + { + krb5_keyblock *key; + + ret = krb5_auth_con_getremotesubkey(context, ac, &key); + if (ret) + goto out; + if (key == NULL) { + ret = EINVAL; + krb5_set_error_message(context, ret, "digest: remote subkey not found"); + goto out; + } + + ret = krb5_crypto_init(context, key, 0, &crypto); + krb5_free_keyblock (context, key); + if (ret) + goto out; + } + + ret = krb5_decrypt_EncryptedData(context, crypto, KRB5_KU_DIGEST_ENCRYPT, + &req->innerReq, &buf); + krb5_crypto_destroy(context, crypto); + crypto = NULL; + if (ret) + goto out; + + ret = decode_DigestReqInner(buf.data, buf.length, &ireq, NULL); + krb5_data_free(&buf); + if (ret) { + krb5_set_error_message(context, ret, "Failed to decode digest inner request"); + goto out; + } + + kdc_log(context, config, 3, "Valid digest request from %s (%s)", + client_name, from); + + /* + * Process the inner request + */ + + switch (ireq.element) { + case choice_DigestReqInner_init: { + unsigned char server_nonce[16], identifier; + + RAND_pseudo_bytes(&identifier, sizeof(identifier)); + RAND_pseudo_bytes(server_nonce, sizeof(server_nonce)); + + server_nonce[0] = kdc_time & 0xff; + server_nonce[1] = (kdc_time >> 8) & 0xff; + server_nonce[2] = (kdc_time >> 16) & 0xff; + server_nonce[3] = (kdc_time >> 24) & 0xff; + + r.element = choice_DigestRepInner_initReply; + + hex_encode(server_nonce, sizeof(server_nonce), &r.u.initReply.nonce); + if (r.u.initReply.nonce == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "Failed to decode server nonce"); + goto out; + } + + sp = krb5_storage_emem(); + if (sp == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + ret = krb5_store_stringz(sp, ireq.u.init.type); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + if (ireq.u.init.channel) { + char *s; + int aret; + + aret = asprintf(&s, "%s-%s:%s", r.u.initReply.nonce, + ireq.u.init.channel->cb_type, + ireq.u.init.channel->cb_binding); + if (aret == -1 || s == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, + "Failed to allocate channel binding"); + goto out; + } + free(r.u.initReply.nonce); + r.u.initReply.nonce = s; + } + + ret = krb5_store_stringz(sp, r.u.initReply.nonce); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + if (strcasecmp(ireq.u.init.type, "CHAP") == 0) { + int aret; + + r.u.initReply.identifier = + malloc(sizeof(*r.u.initReply.identifier)); + if (r.u.initReply.identifier == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + aret = asprintf(r.u.initReply.identifier, "%02X", identifier&0xff); + if (aret == -1 || *r.u.initReply.identifier == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + } else + r.u.initReply.identifier = NULL; + + if (ireq.u.init.hostname) { + ret = krb5_store_stringz(sp, *ireq.u.init.hostname); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + } + + ret = krb5_storage_to_data(sp, &buf); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + ret = get_digest_key(context, config, server, &crypto); + if (ret) + goto out; + + ret = krb5_create_checksum(context, + crypto, + KRB5_KU_DIGEST_OPAQUE, + 0, + buf.data, + buf.length, + &res); + krb5_crypto_destroy(context, crypto); + crypto = NULL; + krb5_data_free(&buf); + if (ret) + goto out; + + ASN1_MALLOC_ENCODE(Checksum, buf.data, buf.length, &res, &size, ret); + free_Checksum(&res); + if (ret) { + krb5_set_error_message(context, ret, "Failed to encode " + "checksum in digest request"); + goto out; + } + if (size != buf.length) + krb5_abortx(context, "ASN1 internal error"); + + hex_encode(buf.data, buf.length, &r.u.initReply.opaque); + free(buf.data); + krb5_data_zero(&buf); + if (r.u.initReply.opaque == NULL) { + krb5_clear_error_message(context); + ret = ENOMEM; + goto out; + } + + kdc_log(context, config, 3, "Digest %s init request successful from %s", + ireq.u.init.type, from); + + break; + } + case choice_DigestReqInner_digestRequest: { + sp = krb5_storage_emem(); + if (sp == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + ret = krb5_store_stringz(sp, ireq.u.digestRequest.type); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + krb5_store_stringz(sp, ireq.u.digestRequest.serverNonce); + + if (ireq.u.digestRequest.hostname) { + ret = krb5_store_stringz(sp, *ireq.u.digestRequest.hostname); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + } + + buf.length = strlen(ireq.u.digestRequest.opaque); + buf.data = malloc(buf.length); + if (buf.data == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + ret = hex_decode(ireq.u.digestRequest.opaque, buf.data, buf.length); + if (ret <= 0) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "Failed to decode opaque"); + goto out; + } + buf.length = ret; + + ret = decode_Checksum(buf.data, buf.length, &res, NULL); + free(buf.data); + krb5_data_zero(&buf); + if (ret) { + krb5_set_error_message(context, ret, + "Failed to decode digest Checksum"); + goto out; + } + + ret = krb5_storage_to_data(sp, &buf); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + serverNonce.length = strlen(ireq.u.digestRequest.serverNonce); + serverNonce.data = malloc(serverNonce.length); + if (serverNonce.data == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + /* + * CHAP does the checksum of the raw nonce, but do it for all + * types, since we need to check the timestamp. + */ + { + ssize_t ssize; + + ssize = hex_decode(ireq.u.digestRequest.serverNonce, + serverNonce.data, serverNonce.length); + if (ssize <= 0) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "Failed to decode serverNonce"); + goto out; + } + serverNonce.length = ssize; + } + + ret = get_digest_key(context, config, server, &crypto); + if (ret) + goto out; + + ret = _kdc_verify_checksum(context, crypto, + KRB5_KU_DIGEST_OPAQUE, + &buf, &res); + free_Checksum(&res); + krb5_data_free(&buf); + krb5_crypto_destroy(context, crypto); + crypto = NULL; + if (ret) + goto out; + + /* verify time */ + { + unsigned char *p = serverNonce.data; + uint32_t t; + + if (serverNonce.length < 4) { + ret = EINVAL; + krb5_set_error_message(context, ret, "server nonce too short"); + goto out; + } + t = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + + if (labs((kdc_time & 0xffffffff) - t) > context->max_skew) { + ret = EINVAL; + krb5_set_error_message(context, ret, "time screw in server nonce "); + goto out; + } + } + + if (strcasecmp(ireq.u.digestRequest.type, "CHAP") == 0) { + EVP_MD_CTX *ctx; + unsigned char md[MD5_DIGEST_LENGTH]; + char *mdx; + char idx; + + if ((config->digests_allowed & CHAP_MD5) == 0) { + kdc_log(context, config, 2, "Digest CHAP MD5 not allowed"); + goto out; + } + + if (ireq.u.digestRequest.identifier == NULL) { + ret = EINVAL; + krb5_set_error_message(context, ret, "Identifier missing " + "from CHAP request"); + goto out; + } + + if (hex_decode(*ireq.u.digestRequest.identifier, &idx, 1) != 1) { + ret = EINVAL; + krb5_set_error_message(context, ret, "failed to decode identifier"); + goto out; + } + + ret = get_password_entry(context, config, + ireq.u.digestRequest.username, + &password); + if (ret) + goto out; + + ctx = EVP_MD_CTX_create(); + + EVP_DigestInit_ex(ctx, EVP_md5(), NULL); + EVP_DigestUpdate(ctx, &idx, 1); + EVP_DigestUpdate(ctx, password, strlen(password)); + EVP_DigestUpdate(ctx, serverNonce.data, serverNonce.length); + EVP_DigestFinal_ex(ctx, md, NULL); + + EVP_MD_CTX_destroy(ctx); + + hex_encode(md, sizeof(md), &mdx); + if (mdx == NULL) { + krb5_clear_error_message(context); + ret = ENOMEM; + goto out; + } + + r.element = choice_DigestRepInner_response; + + ret = strcasecmp(mdx, ireq.u.digestRequest.responseData); + free(mdx); + if (ret == 0) { + r.u.response.success = TRUE; + } else { + kdc_log(context, config, 2, + "CHAP reply mismatch for %s", + ireq.u.digestRequest.username); + r.u.response.success = FALSE; + } + + } else if (strcasecmp(ireq.u.digestRequest.type, "SASL-DIGEST-MD5") == 0) { + EVP_MD_CTX *ctx; + unsigned char md[MD5_DIGEST_LENGTH]; + char *mdx; + char *A1, *A2; + + if ((config->digests_allowed & DIGEST_MD5) == 0) { + kdc_log(context, config, 2, "Digest SASL MD5 not allowed"); + goto out; + } + + if (ireq.u.digestRequest.nonceCount == NULL) + goto out; + if (ireq.u.digestRequest.clientNonce == NULL) + goto out; + if (ireq.u.digestRequest.qop == NULL) + goto out; + if (ireq.u.digestRequest.realm == NULL) + goto out; + + ret = get_password_entry(context, config, + ireq.u.digestRequest.username, + &password); + if (ret) + goto failed; + + ctx = EVP_MD_CTX_create(); + + EVP_DigestInit_ex(ctx, EVP_md5(), NULL); + EVP_DigestUpdate(ctx, ireq.u.digestRequest.username, + strlen(ireq.u.digestRequest.username)); + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, *ireq.u.digestRequest.realm, + strlen(*ireq.u.digestRequest.realm)); + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, password, strlen(password)); + EVP_DigestFinal_ex(ctx, md, NULL); + + EVP_DigestInit_ex(ctx, EVP_md5(), NULL); + EVP_DigestUpdate(ctx, md, sizeof(md)); + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, ireq.u.digestRequest.serverNonce, + strlen(ireq.u.digestRequest.serverNonce)); + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, *ireq.u.digestRequest.nonceCount, + strlen(*ireq.u.digestRequest.nonceCount)); + if (ireq.u.digestRequest.authid) { + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, *ireq.u.digestRequest.authid, + strlen(*ireq.u.digestRequest.authid)); + } + EVP_DigestFinal_ex(ctx, md, NULL); + hex_encode(md, sizeof(md), &A1); + if (A1 == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + EVP_MD_CTX_destroy(ctx); + goto failed; + } + + EVP_DigestInit_ex(ctx, EVP_md5(), NULL); + EVP_DigestUpdate(ctx, + "AUTHENTICATE:", sizeof("AUTHENTICATE:") - 1); + EVP_DigestUpdate(ctx, *ireq.u.digestRequest.uri, + strlen(*ireq.u.digestRequest.uri)); + + /* conf|int */ + if (strcmp(ireq.u.digestRequest.digest, "clear") != 0) { + static char conf_zeros[] = ":00000000000000000000000000000000"; + EVP_DigestUpdate(ctx, conf_zeros, sizeof(conf_zeros) - 1); + } + + EVP_DigestFinal_ex(ctx, md, NULL); + + hex_encode(md, sizeof(md), &A2); + if (A2 == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + free(A1); + goto failed; + } + + EVP_DigestInit_ex(ctx, EVP_md5(), NULL); + EVP_DigestUpdate(ctx, A1, strlen(A2)); + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, ireq.u.digestRequest.serverNonce, + strlen(ireq.u.digestRequest.serverNonce)); + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, *ireq.u.digestRequest.nonceCount, + strlen(*ireq.u.digestRequest.nonceCount)); + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, *ireq.u.digestRequest.clientNonce, + strlen(*ireq.u.digestRequest.clientNonce)); + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, *ireq.u.digestRequest.qop, + strlen(*ireq.u.digestRequest.qop)); + EVP_DigestUpdate(ctx, ":", 1); + EVP_DigestUpdate(ctx, A2, strlen(A2)); + + EVP_DigestFinal_ex(ctx, md, NULL); + + EVP_MD_CTX_destroy(ctx); + + free(A1); + free(A2); + + hex_encode(md, sizeof(md), &mdx); + if (mdx == NULL) { + krb5_clear_error_message(context); + ret = ENOMEM; + goto out; + } + + r.element = choice_DigestRepInner_response; + ret = strcasecmp(mdx, ireq.u.digestRequest.responseData); + free(mdx); + if (ret == 0) { + r.u.response.success = TRUE; + } else { + kdc_log(context, config, 2, + "DIGEST-MD5 reply mismatch for %s", + ireq.u.digestRequest.username); + r.u.response.success = FALSE; + } + + } else if (strcasecmp(ireq.u.digestRequest.type, "MS-CHAP-V2") == 0) { + unsigned char md[SHA_DIGEST_LENGTH], challenge[SHA_DIGEST_LENGTH]; + krb5_principal clientprincipal = NULL; + char *mdx; + const char *username; + struct ntlm_buf answer; + Key *key = NULL; + EVP_MD_CTX *ctp; + + if ((config->digests_allowed & MS_CHAP_V2) == 0) { + kdc_log(context, config, 2, "MS-CHAP-V2 not allowed"); + goto failed; + } + + if (ireq.u.digestRequest.clientNonce == NULL) { + ret = EINVAL; + krb5_set_error_message(context, ret, + "MS-CHAP-V2 clientNonce missing"); + goto failed; + } + if (serverNonce.length != 16) { + ret = EINVAL; + krb5_set_error_message(context, ret, + "MS-CHAP-V2 serverNonce wrong length"); + goto failed; + } + + /* strip of the domain component */ + username = strchr(ireq.u.digestRequest.username, '\\'); + if (username == NULL) + username = ireq.u.digestRequest.username; + else + username++; + + ctp = EVP_MD_CTX_create(); + + /* ChallengeHash */ + EVP_DigestInit_ex(ctp, EVP_sha1(), NULL); + { + ssize_t ssize; + krb5_data clientNonce; + + clientNonce.length = strlen(*ireq.u.digestRequest.clientNonce); + clientNonce.data = malloc(clientNonce.length); + if (clientNonce.data == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, + "malloc: out of memory"); + EVP_MD_CTX_destroy(ctp); + goto out; + } + + ssize = hex_decode(*ireq.u.digestRequest.clientNonce, + clientNonce.data, clientNonce.length); + if (ssize != 16) { + ret = ENOMEM; + krb5_set_error_message(context, ret, + "Failed to decode clientNonce"); + EVP_MD_CTX_destroy(ctp); + goto out; + } + EVP_DigestUpdate(ctp, clientNonce.data, ssize); + free(clientNonce.data); + } + EVP_DigestUpdate(ctp, serverNonce.data, serverNonce.length); + EVP_DigestUpdate(ctp, username, strlen(username)); + + EVP_DigestFinal_ex(ctp, challenge, NULL); + + EVP_MD_CTX_destroy(ctp); + + /* NtPasswordHash */ + ret = krb5_parse_name(context, username, &clientprincipal); + if (ret) + goto failed; + + ret = _kdc_db_fetch(context, config, clientprincipal, + HDB_F_GET_CLIENT, NULL, &userdb, &user); + krb5_free_principal(context, clientprincipal); + if (ret) { + krb5_set_error_message(context, ret, + "MS-CHAP-V2 user %s not in database", + username); + goto failed; + } + + ret = hdb_enctype2key(context, user, NULL, + ETYPE_ARCFOUR_HMAC_MD5, &key); + if (ret) { + krb5_set_error_message(context, ret, + "MS-CHAP-V2 missing arcfour key %s", + username); + goto failed; + } + + /* ChallengeResponse */ + ret = heim_ntlm_calculate_ntlm1(key->key.keyvalue.data, + key->key.keyvalue.length, + challenge, &answer); + if (ret) { + krb5_set_error_message(context, ret, "NTLM missing arcfour key"); + goto failed; + } + + hex_encode(answer.data, answer.length, &mdx); + if (mdx == NULL) { + free(answer.data); + krb5_clear_error_message(context); + ret = ENOMEM; + goto out; + } + + r.element = choice_DigestRepInner_response; + ret = strcasecmp(mdx, ireq.u.digestRequest.responseData); + if (ret == 0) { + r.u.response.success = TRUE; + } else { + kdc_log(context, config, 2, + "MS-CHAP-V2 hash mismatch for %s", + ireq.u.digestRequest.username); + r.u.response.success = FALSE; + } + free(mdx); + + if (r.u.response.success) { + unsigned char hashhash[MD4_DIGEST_LENGTH]; + EVP_MD_CTX *ctxp; + + ctxp = EVP_MD_CTX_create(); + + /* hashhash */ + { + EVP_DigestInit_ex(ctxp, EVP_md4(), NULL); + EVP_DigestUpdate(ctxp, + key->key.keyvalue.data, + key->key.keyvalue.length); + EVP_DigestFinal_ex(ctxp, hashhash, NULL); + } + + /* GenerateAuthenticatorResponse */ + EVP_DigestInit_ex(ctxp, EVP_sha1(), NULL); + EVP_DigestUpdate(ctxp, hashhash, sizeof(hashhash)); + EVP_DigestUpdate(ctxp, answer.data, answer.length); + EVP_DigestUpdate(ctxp, ms_chap_v2_magic1, + sizeof(ms_chap_v2_magic1)); + EVP_DigestFinal_ex(ctxp, md, NULL); + + EVP_DigestInit_ex(ctxp, EVP_sha1(), NULL); + EVP_DigestUpdate(ctxp, md, sizeof(md)); + EVP_DigestUpdate(ctxp, challenge, 8); + EVP_DigestUpdate(ctxp, ms_chap_v2_magic2, + sizeof(ms_chap_v2_magic2)); + EVP_DigestFinal_ex(ctxp, md, NULL); + + r.u.response.rsp = calloc(1, sizeof(*r.u.response.rsp)); + if (r.u.response.rsp == NULL) { + free(answer.data); + krb5_clear_error_message(context); + EVP_MD_CTX_destroy(ctxp); + ret = ENOMEM; + goto out; + } + + hex_encode(md, sizeof(md), r.u.response.rsp); + if (r.u.response.rsp == NULL) { + free(answer.data); + krb5_clear_error_message(context); + EVP_MD_CTX_destroy(ctxp); + ret = ENOMEM; + goto out; + } + + /* get_master, rfc 3079 3.4 */ + EVP_DigestInit_ex(ctxp, EVP_sha1(), NULL); + EVP_DigestUpdate(ctxp, hashhash, 16); + EVP_DigestUpdate(ctxp, answer.data, answer.length); + EVP_DigestUpdate(ctxp, ms_rfc3079_magic1, + sizeof(ms_rfc3079_magic1)); + EVP_DigestFinal_ex(ctxp, md, NULL); + + free(answer.data); + + EVP_MD_CTX_destroy(ctxp); + + r.u.response.session_key = + calloc(1, sizeof(*r.u.response.session_key)); + if (r.u.response.session_key == NULL) { + krb5_clear_error_message(context); + ret = ENOMEM; + goto out; + } + + ret = krb5_data_copy(r.u.response.session_key, md, 16); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + } + + } else { + int aret; + + r.element = choice_DigestRepInner_error; + aret = asprintf(&r.u.error.reason, "Unsupported digest type %s", + ireq.u.digestRequest.type); + if (aret == -1 || r.u.error.reason == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + r.u.error.code = EINVAL; + } + + kdc_log(context, config, 3, "Digest %s request successful %s", + ireq.u.digestRequest.type, ireq.u.digestRequest.username); + + break; + } + case choice_DigestReqInner_ntlmInit: + + if ((config->digests_allowed & (NTLM_V1|NTLM_V1_SESSION|NTLM_V2)) == 0) { + kdc_log(context, config, 2, "NTLM not allowed"); + goto failed; + } + + r.element = choice_DigestRepInner_ntlmInitReply; + + r.u.ntlmInitReply.flags = NTLM_NEG_UNICODE; + + if ((ireq.u.ntlmInit.flags & NTLM_NEG_UNICODE) == 0) { + kdc_log(context, config, 2, "NTLM client have no unicode"); + goto failed; + } + + if (ireq.u.ntlmInit.flags & NTLM_NEG_NTLM) + r.u.ntlmInitReply.flags |= NTLM_NEG_NTLM; + else { + kdc_log(context, config, 2, "NTLM client doesn't support NTLM"); + goto failed; + } + + r.u.ntlmInitReply.flags |= + NTLM_NEG_TARGET | + NTLM_TARGET_DOMAIN | + NTLM_ENC_128; + +#define ALL \ + NTLM_NEG_SIGN| \ + NTLM_NEG_SEAL| \ + NTLM_NEG_ALWAYS_SIGN| \ + NTLM_NEG_NTLM2_SESSION| \ + NTLM_NEG_KEYEX + + r.u.ntlmInitReply.flags |= (ireq.u.ntlmInit.flags & (ALL)); + +#undef ALL + + r.u.ntlmInitReply.targetname = + get_ntlm_targetname(context, client); + if (r.u.ntlmInitReply.targetname == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + r.u.ntlmInitReply.challenge.data = malloc(8); + if (r.u.ntlmInitReply.challenge.data == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + r.u.ntlmInitReply.challenge.length = 8; + if (RAND_bytes(r.u.ntlmInitReply.challenge.data, + r.u.ntlmInitReply.challenge.length) != 1) + { + ret = ENOMEM; + krb5_set_error_message(context, ret, "out of random error"); + goto out; + } + /* XXX fix targetinfo */ + ALLOC(r.u.ntlmInitReply.targetinfo); + if (r.u.ntlmInitReply.targetinfo == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + ret = fill_targetinfo(context, + r.u.ntlmInitReply.targetname, + client, + r.u.ntlmInitReply.targetinfo); + if (ret) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + /* + * Save data encryted in opaque for the second part of the + * ntlm authentication + */ + sp = krb5_storage_emem(); + if (sp == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + ret = krb5_storage_write(sp, r.u.ntlmInitReply.challenge.data, 8); + if (ret != 8) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "storage write challenge"); + goto out; + } + ret = krb5_store_uint32(sp, r.u.ntlmInitReply.flags); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + ret = krb5_storage_to_data(sp, &buf); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + ret = get_digest_key(context, config, server, &crypto); + if (ret) + goto out; + + ret = krb5_encrypt(context, crypto, KRB5_KU_DIGEST_OPAQUE, + buf.data, buf.length, &r.u.ntlmInitReply.opaque); + krb5_data_free(&buf); + krb5_crypto_destroy(context, crypto); + crypto = NULL; + if (ret) + goto out; + + kdc_log(context, config, 3, "NTLM init from %s", from); + + break; + + case choice_DigestReqInner_ntlmRequest: { + krb5_principal clientprincipal; + unsigned char sessionkey[16]; + unsigned char challenge[8]; + uint32_t flags; + Key *key = NULL; + int version; + + r.element = choice_DigestRepInner_ntlmResponse; + r.u.ntlmResponse.success = 0; + r.u.ntlmResponse.flags = 0; + r.u.ntlmResponse.sessionkey = NULL; + r.u.ntlmResponse.tickets = NULL; + + /* get username */ + ret = krb5_parse_name(context, + ireq.u.ntlmRequest.username, + &clientprincipal); + if (ret) + goto failed; + + ret = _kdc_db_fetch(context, config, clientprincipal, + HDB_F_GET_CLIENT, NULL, &userdb, &user); + krb5_free_principal(context, clientprincipal); + if (ret) { + krb5_set_error_message(context, ret, "NTLM user %s not in database", + ireq.u.ntlmRequest.username); + goto failed; + } + + ret = get_digest_key(context, config, server, &crypto); + if (ret) + goto failed; + + ret = krb5_decrypt(context, crypto, KRB5_KU_DIGEST_OPAQUE, + ireq.u.ntlmRequest.opaque.data, + ireq.u.ntlmRequest.opaque.length, &buf); + krb5_crypto_destroy(context, crypto); + crypto = NULL; + if (ret) { + kdc_log(context, config, 2, + "Failed to decrypt nonce from %s", from); + goto failed; + } + + sp = krb5_storage_from_data(&buf); + if (sp == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + ret = krb5_storage_read(sp, challenge, sizeof(challenge)); + if (ret != sizeof(challenge)) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "NTLM storage read challenge"); + goto out; + } + ret = krb5_ret_uint32(sp, &flags); + if (ret) { + krb5_set_error_message(context, ret, "NTLM storage read flags"); + goto out; + } + krb5_storage_free(sp); + sp = NULL; + krb5_data_free(&buf); + + if ((flags & NTLM_NEG_NTLM) == 0) { + ret = EINVAL; + krb5_set_error_message(context, ret, "NTLM not negotiated"); + goto out; + } + + ret = hdb_enctype2key(context, user, NULL, + ETYPE_ARCFOUR_HMAC_MD5, &key); + if (ret) { + krb5_set_error_message(context, ret, "NTLM missing arcfour key"); + goto out; + } + + /* check if this is NTLMv2 */ + if (ireq.u.ntlmRequest.ntlm.length != 24) { + struct ntlm_buf infotarget, answer; + char *targetname; + + if ((config->digests_allowed & NTLM_V2) == 0) { + kdc_log(context, config, 2, "NTLM v2 not allowed"); + goto out; + } + + version = 2; + + targetname = get_ntlm_targetname(context, client); + if (targetname == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + answer.length = ireq.u.ntlmRequest.ntlm.length; + answer.data = ireq.u.ntlmRequest.ntlm.data; + + ret = heim_ntlm_verify_ntlm2(key->key.keyvalue.data, + key->key.keyvalue.length, + ireq.u.ntlmRequest.username, + targetname, + 0, + challenge, + &answer, + &infotarget, + sessionkey); + free(targetname); + if (ret) { + krb5_set_error_message(context, ret, "NTLM v2 verify failed"); + goto failed; + } + + /* XXX verify infotarget matches client (checksum ?) */ + + free(infotarget.data); + /* */ + + } else { + struct ntlm_buf answer; + + version = 1; + + if (flags & NTLM_NEG_NTLM2_SESSION) { + unsigned char sessionhash[MD5_DIGEST_LENGTH]; + EVP_MD_CTX *ctx; + + if ((config->digests_allowed & NTLM_V1_SESSION) == 0) { + kdc_log(context, config, 2, "NTLM v1-session not allowed"); + ret = EINVAL; + goto failed; + } + + if (ireq.u.ntlmRequest.lm.length != 24) { + ret = EINVAL; + krb5_set_error_message(context, ret, "LM hash have wrong length " + "for NTLM session key"); + goto failed; + } + + ctx = EVP_MD_CTX_create(); + + EVP_DigestInit_ex(ctx, EVP_md5(), NULL); + + EVP_DigestUpdate(ctx, challenge, sizeof(challenge)); + EVP_DigestUpdate(ctx, ireq.u.ntlmRequest.lm.data, 8); + EVP_DigestFinal_ex(ctx, sessionhash, NULL); + memcpy(challenge, sessionhash, sizeof(challenge)); + + EVP_MD_CTX_destroy(ctx); + + } else { + if ((config->digests_allowed & NTLM_V1) == 0) { + kdc_log(context, config, 2, "NTLM v1 not allowed"); + goto failed; + } + } + + ret = heim_ntlm_calculate_ntlm1(key->key.keyvalue.data, + key->key.keyvalue.length, + challenge, &answer); + if (ret) { + krb5_set_error_message(context, ret, "NTLM missing arcfour key"); + goto failed; + } + + if (ireq.u.ntlmRequest.ntlm.length != answer.length || + ct_memcmp(ireq.u.ntlmRequest.ntlm.data, answer.data, answer.length) != 0) + { + free(answer.data); + ret = EINVAL; + krb5_set_error_message(context, ret, "NTLM hash mismatch"); + goto failed; + } + free(answer.data); + + { + EVP_MD_CTX *ctx; + + ctx = EVP_MD_CTX_create(); + + EVP_DigestInit_ex(ctx, EVP_md4(), NULL); + EVP_DigestUpdate(ctx, + key->key.keyvalue.data, + key->key.keyvalue.length); + EVP_DigestFinal_ex(ctx, sessionkey, NULL); + + EVP_MD_CTX_destroy(ctx); + } + } + + if (ireq.u.ntlmRequest.sessionkey) { + unsigned char masterkey[MD4_DIGEST_LENGTH]; + EVP_CIPHER_CTX rc4; + size_t len; + + if ((flags & NTLM_NEG_KEYEX) == 0) { + ret = EINVAL; + krb5_set_error_message(context, ret, + "NTLM client failed to neg key " + "exchange but still sent key"); + goto failed; + } + + len = ireq.u.ntlmRequest.sessionkey->length; + if (len != sizeof(masterkey)){ + ret = EINVAL; + krb5_set_error_message(context, ret, + "NTLM master key wrong length: %lu", + (unsigned long)len); + goto failed; + } + + + EVP_CIPHER_CTX_init(&rc4); + EVP_CipherInit_ex(&rc4, EVP_rc4(), NULL, sessionkey, NULL, 1); + EVP_Cipher(&rc4, + masterkey, ireq.u.ntlmRequest.sessionkey->data, + sizeof(masterkey)); + EVP_CIPHER_CTX_cleanup(&rc4); + + r.u.ntlmResponse.sessionkey = + malloc(sizeof(*r.u.ntlmResponse.sessionkey)); + if (r.u.ntlmResponse.sessionkey == NULL) { + ret = EINVAL; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + ret = krb5_data_copy(r.u.ntlmResponse.sessionkey, + masterkey, sizeof(masterkey)); + if (ret) { + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + } + + r.u.ntlmResponse.success = 1; + kdc_log(context, config, 0, "NTLM version %d successful for %s", + version, ireq.u.ntlmRequest.username); + break; + } + case choice_DigestReqInner_supportedMechs: + + kdc_log(context, config, 4, "digest supportedMechs from %s", from); + + r.element = choice_DigestRepInner_supportedMechs; + memset(&r.u.supportedMechs, 0, sizeof(r.u.supportedMechs)); + + if (config->digests_allowed & NTLM_V1) + r.u.supportedMechs.ntlm_v1 = 1; + if (config->digests_allowed & NTLM_V1_SESSION) + r.u.supportedMechs.ntlm_v1_session = 1; + if (config->digests_allowed & NTLM_V2) + r.u.supportedMechs.ntlm_v2 = 1; + if (config->digests_allowed & DIGEST_MD5) + r.u.supportedMechs.digest_md5 = 1; + if (config->digests_allowed & CHAP_MD5) + r.u.supportedMechs.chap_md5 = 1; + if (config->digests_allowed & MS_CHAP_V2) + r.u.supportedMechs.ms_chap_v2 = 1; + break; + + default: { + const char *s; + ret = EINVAL; + krb5_set_error_message(context, ret, "unknown operation to digest"); + + failed: + + s = krb5_get_error_message(context, ret); + if (s == NULL) { + krb5_clear_error_message(context); + goto out; + } + + kdc_log(context, config, 2, "Digest failed with: %s", s); + + r.element = choice_DigestRepInner_error; + r.u.error.reason = strdup("unknown error"); + krb5_free_error_message(context, s); + if (r.u.error.reason == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + r.u.error.code = EINVAL; + break; + } + } + + ASN1_MALLOC_ENCODE(DigestRepInner, buf.data, buf.length, &r, &size, ret); + if (ret) { + krb5_set_error_message(context, ret, "Failed to encode inner digest reply"); + goto out; + } + if (size != buf.length) + krb5_abortx(context, "ASN1 internal error"); + + krb5_auth_con_addflags(context, ac, KRB5_AUTH_CONTEXT_USE_SUBKEY, NULL); + + ret = krb5_mk_rep (context, ac, &rep.apRep); + if (ret) + goto out; + + { + krb5_keyblock *key; + + ret = krb5_auth_con_getlocalsubkey(context, ac, &key); + if (ret) + goto out; + + ret = krb5_crypto_init(context, key, 0, &crypto); + krb5_free_keyblock (context, key); + if (ret) + goto out; + } + + ret = krb5_encrypt_EncryptedData(context, crypto, KRB5_KU_DIGEST_ENCRYPT, + buf.data, buf.length, 0, + &rep.innerRep); + if (ret) { + krb5_prepend_error_message(context, ret, "Failed to encrypt digest: "); + goto out; + } + + ASN1_MALLOC_ENCODE(DigestREP, reply->data, reply->length, &rep, &size, ret); + if (ret) { + krb5_set_error_message(context, ret, "Failed to encode digest reply"); + goto out; + } + if (size != reply->length) + krb5_abortx(context, "ASN1 internal error"); + + + out: + if (ac) + krb5_auth_con_free(context, ac); + if (ret) + krb5_warn(context, ret, "Digest request from %s failed", from); + if (ticket) + krb5_free_ticket(context, ticket); + if (id) + krb5_kt_close(context, id); + if (crypto) + krb5_crypto_destroy(context, crypto); + if (sp) + krb5_storage_free(sp); + if (user) + _kdc_free_ent (context, userdb, user); + if (server) + _kdc_free_ent (context, serverdb, server); + if (client) + _kdc_free_ent (context, clientdb, client); + if (password) { + memset(password, 0, strlen(password)); + free (password); + } + if (client_name) + free (client_name); + krb5_data_free(&buf); + krb5_data_free(&serverNonce); + free_Checksum(&res); + free_DigestREP(&rep); + free_DigestRepInner(&r); + free_DigestReqInner(&ireq); + + return ret; +} + +#endif /* DIGEST */ diff --git a/third_party/heimdal/kdc/fast.c b/third_party/heimdal/kdc/fast.c new file mode 100644 index 0000000..bc77f74 --- /dev/null +++ b/third_party/heimdal/kdc/fast.c @@ -0,0 +1,1002 @@ +/* + * Copyright (c) 1997-2011 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2010 - 2011 Apple Inc. 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. + */ + +#include "kdc_locl.h" + +static krb5_error_code +salt_fastuser_crypto(astgs_request_t r, + krb5_const_principal salt_principal, + krb5_enctype enctype, + krb5_crypto fast_crypto, + krb5_crypto *salted_crypto) +{ + krb5_error_code ret; + krb5_principal client_princ = NULL; + krb5_data salt; + krb5_keyblock dkey; + size_t size; + + *salted_crypto = NULL; + + krb5_data_zero(&salt); + krb5_keyblock_zero(&dkey); + + if (salt_principal == NULL) { + if (r->req.req_body.cname == NULL) { + ret = KRB5KRB_ERR_GENERIC; + goto out; + } + + ret = _krb5_principalname2krb5_principal(r->context, &client_princ, + *(r->req.req_body.cname), + r->req.req_body.realm); + if (ret) + goto out; + + salt_principal = client_princ; + } + + ret = krb5_unparse_name(r->context, salt_principal, (char **)&salt.data); + if (ret) + goto out; + + salt.length = strlen(salt.data); + + kdc_log(r->context, r->config, 10, + "salt_fastuser_crypto: salt principal is %s (%d)", + (char *)salt.data, enctype); + + ret = krb5_enctype_keysize(r->context, enctype, &size); + if (ret) + goto out; + + ret = krb5_crypto_prfplus(r->context, fast_crypto, &salt, + size, &dkey.keyvalue); + if (ret) + goto out; + + dkey.keytype = enctype; + + ret = krb5_crypto_init(r->context, &dkey, ENCTYPE_NULL, salted_crypto); + if (ret) + goto out; + +out: + krb5_free_keyblock_contents(r->context, &dkey); + krb5_data_free(&salt); + krb5_free_principal(r->context, client_princ); + + return ret; +} + +static krb5_error_code +get_fastuser_crypto(astgs_request_t r, + krb5_const_principal ticket_client, + krb5_enctype enctype, + krb5_crypto *crypto) +{ + krb5_principal fast_princ; + HDB *fast_db; + hdb_entry *fast_user = NULL; + Key *cookie_key = NULL; + krb5_crypto fast_crypto = NULL; + krb5_error_code ret; + + *crypto = NULL; + + ret = krb5_make_principal(r->context, &fast_princ, + KRB5_WELLKNOWN_ORG_H5L_REALM, + KRB5_WELLKNOWN_NAME, "org.h5l.fast-cookie", NULL); + if (ret) + goto out; + + ret = _kdc_db_fetch(r->context, r->config, fast_princ, + HDB_F_GET_FAST_COOKIE, NULL, &fast_db, &fast_user); + if (ret) + goto out; + + if (enctype == KRB5_ENCTYPE_NULL) + ret = _kdc_get_preferred_key(r->context, r->config, fast_user, + "fast-cookie", &enctype, &cookie_key); + else + ret = hdb_enctype2key(r->context, fast_user, NULL, + enctype, &cookie_key); + if (ret) + goto out; + + ret = krb5_crypto_init(r->context, &cookie_key->key, + ENCTYPE_NULL, &fast_crypto); + if (ret) + goto out; + + ret = salt_fastuser_crypto(r, ticket_client, + cookie_key->key.keytype, + fast_crypto, crypto); + if (ret) + goto out; + + out: + if (fast_user) + _kdc_free_ent(r->context, fast_db, fast_user); + if (fast_crypto) + krb5_crypto_destroy(r->context, fast_crypto); + krb5_free_principal(r->context, fast_princ); + + return ret; +} + + +static krb5_error_code +fast_parse_cookie(astgs_request_t r, + krb5_const_principal ticket_client, + const PA_DATA *pa) +{ + krb5_crypto crypto = NULL; + krb5_error_code ret; + KDCFastCookie data; + krb5_data d1; + size_t len; + + ret = decode_KDCFastCookie(pa->padata_value.data, + pa->padata_value.length, + &data, &len); + if (ret) + return ret; + + if (len != pa->padata_value.length || strcmp("H5L1", data.version) != 0) { + free_KDCFastCookie(&data); + return KRB5KDC_ERR_POLICY; + } + + ret = get_fastuser_crypto(r, ticket_client, data.cookie.etype, &crypto); + if (ret) + goto out; + + ret = krb5_decrypt_EncryptedData(r->context, crypto, + KRB5_KU_H5L_COOKIE, + &data.cookie, &d1); + krb5_crypto_destroy(r->context, crypto); + if (ret) + goto out; + + ret = decode_KDCFastState(d1.data, d1.length, &r->fast, &len); + krb5_data_free(&d1); + if (ret) + goto out; + + if (r->fast.expiration < kdc_time) { + kdc_log(r->context, r->config, 2, "FAST cookie expired"); + ret = KRB5KDC_ERR_POLICY; + goto out; + } + + out: + free_KDCFastCookie(&data); + + return ret; +} + +static krb5_error_code +fast_add_cookie(astgs_request_t r, + krb5_const_principal ticket_client, + METHOD_DATA *method_data) +{ + krb5_crypto crypto = NULL; + KDCFastCookie shell; + krb5_error_code ret; + krb5_data data; + size_t size; + + memset(&shell, 0, sizeof(shell)); + + r->fast.expiration = kdc_time + FAST_EXPIRATION_TIME; + + ASN1_MALLOC_ENCODE(KDCFastState, data.data, data.length, + &r->fast, &size, ret); + if (ret) + return ret; + heim_assert(size == data.length, "internal asn.1 encoder error"); + + ret = get_fastuser_crypto(r, ticket_client, KRB5_ENCTYPE_NULL, &crypto); + if (ret) { + kdc_log(r->context, r->config, 0, + "Failed to find FAST principal for cookie encryption: %d", ret); + goto out; + } + + ret = krb5_encrypt_EncryptedData(r->context, crypto, + KRB5_KU_H5L_COOKIE, + data.data, data.length, 0, + &shell.cookie); + krb5_crypto_destroy(r->context, crypto); + if (ret) + goto out; + + krb5_data_free(&data); + + shell.version = "H5L1"; + + ASN1_MALLOC_ENCODE(KDCFastCookie, data.data, data.length, + &shell, &size, ret); + free_EncryptedData(&shell.cookie); + if (ret) + goto out; + heim_assert(size == data.length, "internal asn.1 encoder error"); + + ret = krb5_padata_add(r->context, method_data, + KRB5_PADATA_FX_COOKIE, + data.data, data.length); + if (ret == 0) + krb5_data_zero(&data); + + out: + krb5_data_free(&data); + return ret; +} + +static krb5_error_code +fast_add_dummy_cookie(astgs_request_t r, + METHOD_DATA *method_data) +{ + krb5_error_code ret; + krb5_data data; + const krb5_data *dummy_fast_cookie = &r->config->dummy_fast_cookie; + + if (dummy_fast_cookie->data == NULL) + return 0; + + ret = krb5_data_copy(&data, + dummy_fast_cookie->data, + dummy_fast_cookie->length); + if (ret) + return ret; + + ret = krb5_padata_add(r->context, method_data, + KRB5_PADATA_FX_COOKIE, + data.data, data.length); + if (ret) { + krb5_data_free(&data); + } + + return ret; +} + +krb5_error_code +_kdc_fast_mk_response(krb5_context context, + krb5_crypto armor_crypto, + METHOD_DATA *pa_data, + krb5_keyblock *strengthen_key, + KrbFastFinished *finished, + krb5uint32 nonce, + krb5_data *data) +{ + PA_FX_FAST_REPLY fxfastrep; + KrbFastResponse fastrep; + krb5_error_code ret; + krb5_data buf; + size_t size; + + memset(&fxfastrep, 0, sizeof(fxfastrep)); + memset(&fastrep, 0, sizeof(fastrep)); + krb5_data_zero(data); + + if (pa_data) { + fastrep.padata.val = pa_data->val; + fastrep.padata.len = pa_data->len; + } + fastrep.strengthen_key = strengthen_key; + fastrep.finished = finished; + fastrep.nonce = nonce; + + ASN1_MALLOC_ENCODE(KrbFastResponse, buf.data, buf.length, + &fastrep, &size, ret); + if (ret) + return ret; + heim_assert(size == buf.length, "internal asn.1 encoder error"); + + fxfastrep.element = choice_PA_FX_FAST_REPLY_armored_data; + + ret = krb5_encrypt_EncryptedData(context, + armor_crypto, + KRB5_KU_FAST_REP, + buf.data, + buf.length, + 0, + &fxfastrep.u.armored_data.enc_fast_rep); + krb5_data_free(&buf); + if (ret) + return ret; + + ASN1_MALLOC_ENCODE(PA_FX_FAST_REPLY, data->data, data->length, + &fxfastrep, &size, ret); + free_PA_FX_FAST_REPLY(&fxfastrep); + if (ret) + return ret; + heim_assert(size == data->length, "internal asn.1 encoder error"); + + return 0; +} + + +static krb5_error_code +_kdc_fast_mk_e_data(astgs_request_t r, + METHOD_DATA *error_method, + krb5_crypto armor_crypto, + const KDC_REQ_BODY *req_body, + krb5_error_code outer_error, + krb5_principal error_client, + krb5_principal error_server, + time_t *csec, int *cusec, + krb5_data *e_data) +{ + krb5_error_code ret = 0; + size_t size; + + /* + * FX-COOKIE can be used outside of FAST, e.g. SRP or GSS. + */ + if (armor_crypto || r->fast.fast_state.len) { + if (r->config->enable_fast_cookie) { + kdc_log(r->context, r->config, 5, "Adding FAST cookie for KRB-ERROR"); + ret = fast_add_cookie(r, error_client, error_method); + if (ret) { + kdc_log(r->context, r->config, 1, + "Failed to add FAST cookie: %d", ret); + free_METHOD_DATA(error_method); + return ret; + } + } else { + kdc_log(r->context, r->config, 5, "Adding dummy FAST cookie for KRB-ERROR"); + ret = fast_add_dummy_cookie(r, error_method); + if (ret) { + kdc_log(r->context, r->config, 1, + "Failed to add dummy FAST cookie: %d", ret); + free_METHOD_DATA(error_method); + return ret; + } + } + } + + if (armor_crypto) { + PA_FX_FAST_REPLY fxfastrep; + KrbFastResponse fastrep; + + memset(&fxfastrep, 0, sizeof(fxfastrep)); + memset(&fastrep, 0, sizeof(fastrep)); + + kdc_log(r->context, r->config, 5, "Making FAST inner KRB-ERROR"); + + /* first add the KRB-ERROR to the fast errors */ + + ret = krb5_mk_error(r->context, + outer_error, + r->e_text, + NULL, + error_client, + error_server, + csec, + cusec, + e_data); + if (ret) { + kdc_log(r->context, r->config, 1, + "Failed to make inner KRB-ERROR: %d", ret); + return ret; + } + + ret = krb5_padata_add(r->context, error_method, + KRB5_PADATA_FX_ERROR, + e_data->data, e_data->length); + if (ret) { + kdc_log(r->context, r->config, 1, + "Failed to make add FAST PADATA to inner KRB-ERROR: %d", ret); + krb5_data_free(e_data); + return ret; + } + + r->e_text = NULL; + + ret = _kdc_fast_mk_response(r->context, armor_crypto, + error_method, NULL, NULL, + req_body->nonce, e_data); + free_METHOD_DATA(error_method); + if (ret) { + kdc_log(r->context, r->config, 1, + "Failed to make outer KRB-ERROR: %d", ret); + return ret; + } + + ret = krb5_padata_add(r->context, error_method, + KRB5_PADATA_FX_FAST, + e_data->data, e_data->length); + if (ret) { + kdc_log(r->context, r->config, 1, + "Failed to make add FAST PADATA to outer KRB-ERROR: %d", ret); + return ret; + } + } else + kdc_log(r->context, r->config, 5, "Making non-FAST KRB-ERROR"); + + if (error_method && error_method->len) { + ASN1_MALLOC_ENCODE(METHOD_DATA, e_data->data, e_data->length, + error_method, &size, ret); + if (ret) { + kdc_log(r->context, r->config, 1, + "Failed to make encode METHOD-DATA: %d", ret); + return ret; + } + heim_assert(size == e_data->length, "internal asn.1 encoder error"); + } + + return ret; +} + + +krb5_error_code +_kdc_fast_mk_error(astgs_request_t r, + METHOD_DATA *error_method, + krb5_crypto armor_crypto, + const KDC_REQ_BODY *req_body, + krb5_error_code outer_error, + krb5_principal error_client, + krb5_principal error_server, + time_t *csec, int *cusec, + krb5_data *error_msg) +{ + krb5_error_code ret; + krb5_data _e_data; + krb5_data *e_data = NULL; + + krb5_data_zero(&_e_data); + + heim_assert(r != NULL, "invalid request in _kdc_fast_mk_error"); + + if (r->e_data.length) { + e_data = &r->e_data; + } else { + ret = _kdc_fast_mk_e_data(r, + error_method, + armor_crypto, + req_body, + outer_error, + error_client, + error_server, + csec, cusec, + &_e_data); + if (ret) { + kdc_log(r->context, r->config, 1, + "Failed to make FAST e-data: %d", ret); + return ret; + } + + e_data = &_e_data; + } + + if (armor_crypto) { + if (r->fast.flags.requested_hidden_names) { + error_client = NULL; + error_server = NULL; + } + csec = NULL; + cusec = NULL; + } + + ret = krb5_mk_error(r->context, + outer_error, + r->e_text, + (e_data->length ? e_data : NULL), + error_client, + error_server, + csec, + cusec, + error_msg); + krb5_data_free(&_e_data); + + if (ret) + kdc_log(r->context, r->config, 1, + "Failed to make encode KRB-ERROR: %d", ret); + + return ret; +} + +static krb5_error_code +fast_unwrap_request(astgs_request_t r, + krb5_ticket *tgs_ticket, + krb5_auth_context tgs_ac) +{ + krb5_principal armor_server_principal = NULL; + char *armor_client_principal_name = NULL; + char *armor_server_principal_name = NULL; + PA_FX_FAST_REQUEST fxreq; + krb5_auth_context ac = NULL; + krb5_ticket *ticket = NULL; + krb5_flags ap_req_options; + krb5_keyblock armorkey; + krb5_keyblock explicit_armorkey; + krb5_error_code ret; + krb5_ap_req ap_req; + KrbFastReq fastreq; + const PA_DATA *pa; + krb5_data data; + size_t len; + int i = 0; + + memset(&fxreq, 0, sizeof(fxreq)); + memset(&fastreq, 0, sizeof(fastreq)); + + pa = _kdc_find_padata(&r->req, &i, KRB5_PADATA_FX_FAST); + if (pa == NULL) { + if (tgs_ac && r->fast_asserted) { + kdc_log(r->context, r->config, 1, + "Client asserted FAST but did not include FX-FAST pa-data"); + ret = KRB5KRB_AP_ERR_MODIFIED; + goto out; + } + + kdc_log(r->context, r->config, 10, "Not a FAST request"); + return 0; + } + + ret = decode_PA_FX_FAST_REQUEST(pa->padata_value.data, + pa->padata_value.length, + &fxreq, + &len); + if (ret) { + kdc_log(r->context, r->config, 4, + "Failed to decode PA-FX-FAST-REQUEST: %d", ret); + goto out; + } + + if (fxreq.element != choice_PA_FX_FAST_REQUEST_armored_data) { + kdc_log(r->context, r->config, 4, + "PA-FX-FAST-REQUEST contains unknown type: %d", + (int)fxreq.element); + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto out; + } + + /* + * If check for armor data or it's not a TGS-REQ with implicit + * armor. + */ + if (fxreq.u.armored_data.armor == NULL && tgs_ac == NULL) { + kdc_log(r->context, r->config, 4, + "AS-REQ armor missing"); + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto out; + } + + r->explicit_armor_present = fxreq.u.armored_data.armor != NULL && tgs_ac != NULL; + + /* + * + */ + if (fxreq.u.armored_data.armor != NULL) { + krb5uint32 kvno; + krb5uint32 *kvno_ptr = NULL; + + if (fxreq.u.armored_data.armor->armor_type != 1) { + kdc_log(r->context, r->config, 4, + "Incorrect AS-REQ armor type"); + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto out; + } + + ret = krb5_decode_ap_req(r->context, + &fxreq.u.armored_data.armor->armor_value, + &ap_req); + if(ret) { + kdc_log(r->context, r->config, 4, "Failed to decode AP-REQ"); + goto out; + } + + /* Save that principal that was in the request */ + ret = _krb5_principalname2krb5_principal(r->context, + &armor_server_principal, + ap_req.ticket.sname, + ap_req.ticket.realm); + if (ret) { + free_AP_REQ(&ap_req); + goto out; + } + + if (ap_req.ticket.enc_part.kvno != NULL) { + kvno = *ap_req.ticket.enc_part.kvno; + kvno_ptr = &kvno; + } + + ret = _kdc_db_fetch(r->context, r->config, armor_server_principal, + HDB_F_GET_KRBTGT | HDB_F_DELAY_NEW_KEYS, + kvno_ptr, + &r->armor_serverdb, &r->armor_server); + if(ret == HDB_ERR_NOT_FOUND_HERE) { + free_AP_REQ(&ap_req); + kdc_log(r->context, r->config, 5, + "Armor key does not have secrets at this KDC, " + "need to proxy"); + goto out; + } else if (ret) { + free_AP_REQ(&ap_req); + ret = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; + goto out; + } + + ret = hdb_enctype2key(r->context, r->armor_server, NULL, + ap_req.ticket.enc_part.etype, + &r->armor_key); + if (ret) { + free_AP_REQ(&ap_req); + goto out; + } + + ret = krb5_verify_ap_req2(r->context, &ac, + &ap_req, + armor_server_principal, + &r->armor_key->key, + 0, + &ap_req_options, + &r->armor_ticket, + KRB5_KU_AP_REQ_AUTH); + free_AP_REQ(&ap_req); + if (ret) + goto out; + + ret = krb5_unparse_name(r->context, armor_server_principal, + &armor_server_principal_name); + if (ret) + goto out; + + /* FIXME krb5_verify_ap_req2() also checks this */ + ret = _kdc_verify_flags(r->context, r->config, + &r->armor_ticket->ticket, + armor_server_principal_name); + if (ret) { + kdc_audit_addreason((kdc_request_t)r, + "Armor TGT expired or invalid"); + goto out; + } + ticket = r->armor_ticket; + } else { + heim_assert(tgs_ticket != NULL, "TGS authentication context without ticket"); + ac = tgs_ac; + ticket = tgs_ticket; + } + + (void) krb5_unparse_name(r->context, ticket->client, &armor_client_principal_name); + kdc_audit_addkv((kdc_request_t)r, 0, "armor_client_name", "%s", + armor_client_principal_name ? + armor_client_principal_name : + "<out of memory>"); + + if (ac->remote_subkey == NULL) { + krb5_auth_con_free(r->context, ac); + kdc_log(r->context, r->config, 2, + "FAST AP-REQ remote subkey missing"); + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto out; + } + + r->fast.flags.kdc_verified = + !_kdc_is_anonymous_pkinit(r->context, ticket->client); + + ret = _krb5_fast_armor_key(r->context, + ac->remote_subkey, + &ticket->ticket.key, + &armorkey, + r->explicit_armor_present ? NULL : &r->armor_crypto); + if (ret) + goto out; + + if (r->explicit_armor_present) { + ret = _krb5_fast_explicit_armor_key(r->context, + &armorkey, + tgs_ac->remote_subkey, + &explicit_armorkey, + &r->armor_crypto); + if (ret) + goto out; + + krb5_free_keyblock_contents(r->context, &explicit_armorkey); + } + + krb5_free_keyblock_contents(r->context, &armorkey); + + ret = krb5_decrypt_EncryptedData(r->context, r->armor_crypto, + KRB5_KU_FAST_ENC, + &fxreq.u.armored_data.enc_fast_req, + &data); + if (ret) { + kdc_log(r->context, r->config, 2, + "Failed to decrypt FAST request"); + goto out; + } + + ret = decode_KrbFastReq(data.data, data.length, &fastreq, NULL); + krb5_data_free(&data); + if (ret) + goto out; + + /* + * verify req-checksum of the outer body + */ + if (tgs_ac) { + /* + * -- For TGS, contains the checksum performed over the type + * -- AP-REQ in the PA-TGS-REQ padata. + */ + i = 0; + pa = _kdc_find_padata(&r->req, &i, KRB5_PADATA_TGS_REQ); + if (pa == NULL) { + kdc_log(r->context, r->config, 4, + "FAST TGS request missing TGS-REQ padata"); + ret = KRB5KRB_ERR_GENERIC; + goto out; + } + + ret = _kdc_verify_checksum(r->context, r->armor_crypto, + KRB5_KU_FAST_REQ_CHKSUM, + &pa->padata_value, + &fxreq.u.armored_data.req_checksum); + if (ret) { + kdc_log(r->context, r->config, 2, + "Bad checksum in FAST TGS request"); + goto out; + } + } else { + /* + * -- For AS, contains the checksum performed over the type + * -- KDC-REQ-BODY for the req-body field of the KDC-REQ + * -- structure; + */ + ret = _kdc_verify_checksum(r->context, r->armor_crypto, + KRB5_KU_FAST_REQ_CHKSUM, + &r->req.req_body._save, + &fxreq.u.armored_data.req_checksum); + if (ret) { + kdc_log(r->context, r->config, 2, + "Bad checksum in FAST AS request"); + goto out; + } + } + + /* + * check for unsupported mandatory options + */ + if (FastOptions2int(fastreq.fast_options) & 0xfffc) { + kdc_log(r->context, r->config, 2, + "FAST unsupported mandatory option set"); + ret = KRB5_KDC_ERR_UNKNOWN_CRITICAL_FAST_OPTIONS; + goto out; + } + + r->fast.flags.requested_hidden_names = fastreq.fast_options.hide_client_names; + + /* KDC MUST ignore outer pa data preauth-14 - 6.5.5 */ + if (r->req.padata) + free_METHOD_DATA(r->req.padata); + else + ALLOC(r->req.padata); + + ret = copy_METHOD_DATA(&fastreq.padata, r->req.padata); + if (ret) + goto out; + + free_KDC_REQ_BODY(&r->req.req_body); + ret = copy_KDC_REQ_BODY(&fastreq.req_body, &r->req.req_body); + if (ret) + goto out; + + kdc_log(r->context, r->config, 5, "Client selected FAST"); + + out: + if (ac && ac != tgs_ac) + krb5_auth_con_free(r->context, ac); + + krb5_free_principal(r->context, armor_server_principal); + krb5_xfree(armor_client_principal_name); + krb5_xfree(armor_server_principal_name); + + free_KrbFastReq(&fastreq); + free_PA_FX_FAST_REQUEST(&fxreq); + + return ret; +} + +/* + * + */ +krb5_error_code +_kdc_fast_unwrap_request(astgs_request_t r, + krb5_ticket *tgs_ticket, + krb5_auth_context tgs_ac) +{ + krb5_error_code ret; + const PA_DATA *pa; + int i = 0; + + if (!r->config->enable_fast) + return 0; + + ret = fast_unwrap_request(r, tgs_ticket, tgs_ac); + if (ret) + return ret; + + if (r->config->enable_fast_cookie) { + /* + * FX-COOKIE can be used outside of FAST, e.g. SRP or GSS. + */ + pa = _kdc_find_padata(&r->req, &i, KRB5_PADATA_FX_COOKIE); + if (pa) { + krb5_const_principal ticket_client = NULL; + + if (tgs_ticket) + ticket_client = tgs_ticket->client; + + ret = fast_parse_cookie(r, ticket_client, pa); + } + } + + return ret; +} + +/* + * Strengthen reply key by mixing with a random key that is + * protected by FAST. + */ +krb5_error_code +_kdc_fast_strengthen_reply_key(astgs_request_t r) +{ + if (r->armor_crypto) { + krb5_keyblock new_reply_key; + krb5_error_code ret; + + kdc_log(r->context, r->config, 5, + "FAST strengthen reply key with strengthen-key"); + + heim_assert(r->reply_key.keytype != KRB5_ENCTYPE_NULL, "NULL reply key enctype"); + + ret = krb5_generate_random_keyblock(r->context, r->reply_key.keytype, + &r->strengthen_key); + if (ret) { + kdc_log(r->context, r->config, 0, "failed to prepare random keyblock"); + return ret; + } + + ret = _krb5_fast_cf2(r->context, + &r->strengthen_key, "strengthenkey", + &r->reply_key, "replykey", + &new_reply_key, NULL); + if (ret) + return ret; + + krb5_free_keyblock_contents(r->context, &r->reply_key); + r->reply_key = new_reply_key; + } + + return 0; +} + +/* + * Zero and free KDCFastState + */ +void +_kdc_free_fast_state(KDCFastState *state) +{ + size_t i; + + for (i = 0; i < state->fast_state.len; i++) { + PA_DATA *pa = &state->fast_state.val[i]; + + if (pa->padata_value.data) + memset_s(pa->padata_value.data, 0, + pa->padata_value.length, pa->padata_value.length); + } + free_KDCFastState(state); +} + +krb5_error_code +_kdc_fast_check_armor_pac(astgs_request_t r, int flags) +{ + krb5_error_code ret; + krb5_boolean ad_kdc_issued = FALSE; + krb5_pac mspac = NULL; + krb5_principal armor_client_principal = NULL; + HDB *armor_db; + hdb_entry *armor_client = NULL; + char *armor_client_principal_name = NULL; + + flags |= HDB_F_ARMOR_PRINCIPAL; + if (_kdc_synthetic_princ_used_p(r->context, r->armor_ticket)) + flags |= HDB_F_SYNTHETIC_OK; + if (r->req.req_body.kdc_options.canonicalize) + flags |= HDB_F_CANON; + + ret = _krb5_principalname2krb5_principal(r->context, + &armor_client_principal, + r->armor_ticket->ticket.cname, + r->armor_ticket->ticket.crealm); + if (ret) + goto out; + + ret = krb5_unparse_name(r->context, armor_client_principal, + &armor_client_principal_name); + if (ret) + goto out; + + ret = _kdc_db_fetch_client(r->context, r->config, flags, + armor_client_principal, armor_client_principal_name, + r->req.req_body.realm, &armor_db, &armor_client); + if (ret) + goto out; + + ret = kdc_check_flags(r, FALSE, armor_client, NULL); + if (ret) + goto out; + + ret = _kdc_check_pac(r, armor_client_principal, NULL, + armor_client, r->armor_server, + r->armor_server, r->armor_server, + &r->armor_key->key, &r->armor_key->key, + &r->armor_ticket->ticket, &ad_kdc_issued, &mspac, NULL, NULL); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + + kdc_log(r->context, r->config, 4, + "Verify armor PAC (%s) failed for %s (%s) from %s with %s (%s)", + armor_client_principal_name, r->cname, r->sname, + r->from, msg, mspac ? "Ticket unsigned" : "No PAC"); + + krb5_free_error_message(r->context, msg); + + goto out; + } + + r->armor_clientdb = armor_db; + armor_db = NULL; + + r->armor_client = armor_client; + armor_client = NULL; + + r->armor_pac = mspac; + mspac = NULL; + +out: + krb5_xfree(armor_client_principal_name); + if (armor_client) + _kdc_free_ent(r->context, armor_db, armor_client); + krb5_free_principal(r->context, armor_client_principal); + krb5_pac_free(r->context, mspac); + + return ret; +} diff --git a/third_party/heimdal/kdc/gss_preauth.c b/third_party/heimdal/kdc/gss_preauth.c new file mode 100644 index 0000000..24663de --- /dev/null +++ b/third_party/heimdal/kdc/gss_preauth.c @@ -0,0 +1,1034 @@ +/* + * Copyright (c) 2021, PADL Software Pty Ltd. + * All rights reserved. + * + * Portions Copyright (c) 2019 Kungliga Tekniska Högskolan + * + * 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 PADL Software 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 PADL SOFTWARE 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 PADL SOFTWARE 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. + */ + +#include "kdc_locl.h" + +#include <gssapi/gssapi.h> +#include <gssapi_mech.h> + +#include <gss-preauth-protos.h> +#include <gss-preauth-private.h> + +#include "gss_preauth_authorizer_plugin.h" + +struct gss_client_params { + OM_uint32 major, minor; + gss_ctx_id_t context_handle; + gss_name_t initiator_name; + gss_OID mech_type; + gss_buffer_desc output_token; + OM_uint32 flags; + OM_uint32 lifetime; + krb5_checksum req_body_checksum; +}; + +static void +pa_gss_display_status(astgs_request_t r, + OM_uint32 major, + OM_uint32 minor, + gss_client_params *gcp, + const char *msg); + +static void +pa_gss_display_name(gss_name_t name, + gss_buffer_t namebuf, + gss_const_buffer_t *namebuf_p); + +static void HEIM_CALLCONV +pa_gss_dealloc_client_params(void *ptr); + +/* + * Create a checksum over KDC-REQ-BODY (without the nonce), used to + * assert the request is invariant within the preauth conversation. + */ +static krb5_error_code +pa_gss_create_req_body_checksum(astgs_request_t r, + krb5_checksum *checksum) +{ + krb5_error_code ret; + KDC_REQ_BODY b = r->req.req_body; + krb5_data data; + size_t size; + + b.nonce = 0; + + ASN1_MALLOC_ENCODE(KDC_REQ_BODY, data.data, data.length, &b, &size, ret); + heim_assert(ret || data.length, + "internal asn1 encoder error"); + + ret = krb5_create_checksum(r->context, NULL, 0, CKSUMTYPE_SHA256, + data.data, data.length, checksum); + krb5_data_free(&data); + + return ret; +} + +/* + * Verify a checksum over KDC-REQ-BODY (without the nonce), used to + * assert the request is invariant within the preauth conversation. + */ +static krb5_error_code +pa_gss_verify_req_body_checksum(astgs_request_t r, + krb5_checksum *checksum) +{ + krb5_error_code ret; + KDC_REQ_BODY b = r->req.req_body; + krb5_data data; + size_t size; + + b.nonce = 0; + + ASN1_MALLOC_ENCODE(KDC_REQ_BODY, data.data, data.length, &b, &size, ret); + heim_assert(ret || data.length, + "internal asn1 encoder error"); + + ret = _kdc_verify_checksum(r->context, NULL, 0, &data, checksum); + krb5_data_free(&data); + + return ret; +} + +/* + * Decode the FX-COOKIE context state, consisting of the exported + * GSS context token concatenated with the checksum of the initial + * KDC-REQ-BODY. + */ +static krb5_error_code +pa_gss_decode_context_state(astgs_request_t r, + const krb5_data *state, + gss_buffer_t sec_context_token, + krb5_checksum *req_body_checksum) +{ + krb5_error_code ret; + krb5_storage *sp; + size_t cksumsize; + krb5_data data; + int32_t cksumtype; + + memset(req_body_checksum, 0, sizeof(*req_body_checksum)); + sec_context_token->length = 0; + sec_context_token->value = NULL; + + krb5_data_zero(&data); + + sp = krb5_storage_from_readonly_mem(state->data, state->length); + if (sp == NULL) { + ret = krb5_enomem(r->context); + goto out; + } + + krb5_storage_set_eof_code(sp, KRB5_BAD_MSIZE); + krb5_storage_set_byteorder(sp, KRB5_STORAGE_BYTEORDER_PACKED); + + ret = krb5_ret_data(sp, &data); + if (ret) + goto out; + + ret = krb5_ret_int32(sp, &cksumtype); + if (ret) + goto out; + + req_body_checksum->cksumtype = (CKSUMTYPE)cksumtype; + + if (req_body_checksum->cksumtype == CKSUMTYPE_NONE || + krb5_checksum_is_keyed(r->context, req_body_checksum->cksumtype)) { + ret = KRB5KDC_ERR_SUMTYPE_NOSUPP; + goto out; + } + + ret = krb5_checksumsize(r->context, req_body_checksum->cksumtype, + &cksumsize); + if (ret) + goto out; + + req_body_checksum->checksum.data = malloc(cksumsize); + if (req_body_checksum->checksum.data == NULL) { + ret = krb5_enomem(r->context); + goto out; + } + + if (krb5_storage_read(sp, req_body_checksum->checksum.data, + cksumsize) != cksumsize) { + ret = KRB5_BAD_MSIZE; + goto out; + } + + req_body_checksum->checksum.length = cksumsize; + + _krb5_gss_data_to_buffer(&data, sec_context_token); + +out: + if (ret) { + krb5_data_free(&data); + free_Checksum(req_body_checksum); + memset(req_body_checksum, 0, sizeof(*req_body_checksum)); + } + krb5_storage_free(sp); + + return ret; +} + +/* + * Deserialize a GSS-API security context from the FAST cookie. + */ +static krb5_error_code +pa_gss_get_context_state(astgs_request_t r, + gss_client_params *gcp) +{ + int idx = 0; + PA_DATA *fast_pa; + krb5_error_code ret; + + OM_uint32 major, minor; + gss_buffer_desc sec_context_token; + + fast_pa = krb5_find_padata(r->fast.fast_state.val, + r->fast.fast_state.len, + KRB5_PADATA_GSS, &idx); + if (fast_pa == NULL) + return 0; + + ret = pa_gss_decode_context_state(r, &fast_pa->padata_value, + &sec_context_token, + &gcp->req_body_checksum); + if (ret) + return ret; + + ret = pa_gss_verify_req_body_checksum(r, &gcp->req_body_checksum); + if (ret) { + gss_release_buffer(&minor, &sec_context_token); + return ret; + } + + major = gss_import_sec_context(&minor, &sec_context_token, + &gcp->context_handle); + if (GSS_ERROR(major)) { + pa_gss_display_status(r, major, minor, gcp, + "Failed to import GSS pre-authentication context"); + ret = _krb5_gss_map_error(major, minor); + } else + ret = 0; + + gss_release_buffer(&minor, &sec_context_token); + + return ret; +} + +/* + * Encode the FX-COOKIE context state, consisting of the exported + * GSS context token concatenated with the checksum of the initial + * KDC-REQ-BODY. + */ +static krb5_error_code +pa_gss_encode_context_state(astgs_request_t r, + gss_const_buffer_t sec_context_token, + const krb5_checksum *req_body_checksum, + krb5_data *state) +{ + krb5_error_code ret; + krb5_storage *sp; + krb5_data data; + + krb5_data_zero(state); + + sp = krb5_storage_emem(); + if (sp == NULL) { + ret = krb5_enomem(r->context); + goto out; + } + + krb5_storage_set_byteorder(sp, KRB5_STORAGE_BYTEORDER_PACKED); + + _krb5_gss_buffer_to_data(sec_context_token, &data); + + ret = krb5_store_data(sp, data); + if (ret) + goto out; + + ret = krb5_store_int32(sp, (int32_t)req_body_checksum->cksumtype); + if (ret) + goto out; + + ret = krb5_store_bytes(sp, req_body_checksum->checksum.data, + req_body_checksum->checksum.length); + if (ret) + goto out; + + ret = krb5_storage_to_data(sp, state); + if (ret) + goto out; + +out: + krb5_storage_free(sp); + + return ret; +} + +/* + * Serialize a GSS-API security context into a FAST cookie. + */ +static krb5_error_code +pa_gss_set_context_state(astgs_request_t r, + gss_client_params *gcp) +{ + krb5_error_code ret; + PA_DATA *fast_pa; + int idx = 0; + krb5_data state; + + OM_uint32 major, minor; + gss_buffer_desc sec_context_token = GSS_C_EMPTY_BUFFER; + + /* + * On second and subsequent responses, we can recycle the checksum + * from the request as it is validated and invariant. This saves + * re-encoding the request body again. + */ + if (gcp->req_body_checksum.cksumtype == CKSUMTYPE_NONE) { + ret = pa_gss_create_req_body_checksum(r, &gcp->req_body_checksum); + if (ret) + return ret; + } + + major = gss_export_sec_context(&minor, &gcp->context_handle, + &sec_context_token); + if (GSS_ERROR(major)) { + pa_gss_display_status(r, major, minor, gcp, + "Failed to export GSS pre-authentication context"); + return _krb5_gss_map_error(major, minor); + } + + ret = pa_gss_encode_context_state(r, &sec_context_token, + &gcp->req_body_checksum, &state); + gss_release_buffer(&minor, &sec_context_token); + if (ret) + return ret; + + fast_pa = krb5_find_padata(r->fast.fast_state.val, + r->fast.fast_state.len, + KRB5_PADATA_GSS, &idx); + if (fast_pa) { + krb5_data_free(&fast_pa->padata_value); + fast_pa->padata_value = state; + } else { + ret = krb5_padata_add(r->context, &r->fast.fast_state, + KRB5_PADATA_GSS, + state.data, state.length); + if (ret) + krb5_data_free(&state); + } + + return ret; +} + +static krb5_error_code +pa_gss_acquire_acceptor_cred(astgs_request_t r, + gss_client_params *gcp, + gss_cred_id_t *cred) +{ + krb5_error_code ret; + krb5_principal tgs_name; + + OM_uint32 major, minor; + gss_name_t target_name = GSS_C_NO_NAME; + gss_buffer_desc display_name = GSS_C_EMPTY_BUFFER; + gss_const_buffer_t display_name_p; + + *cred = GSS_C_NO_CREDENTIAL; + + ret = krb5_make_principal(r->context, &tgs_name, r->req.req_body.realm, + KRB5_TGS_NAME, r->req.req_body.realm, NULL); + if (ret) + return ret; + + ret = _krb5_gss_pa_unparse_name(r->context, tgs_name, &target_name); + krb5_free_principal(r->context, tgs_name); + if (ret) + return ret; + + pa_gss_display_name(target_name, &display_name, &display_name_p); + + kdc_log(r->context, r->config, 4, + "Acquiring GSS acceptor credential for %.*s", + (int)display_name_p->length, (char *)display_name_p->value); + + major = gss_acquire_cred(&minor, target_name, GSS_C_INDEFINITE, + r->config->gss_mechanisms_allowed, + GSS_C_ACCEPT, cred, NULL, NULL); + ret = _krb5_gss_map_error(major, minor); + + if (ret) + pa_gss_display_status(r, major, minor, gcp, + "Failed to acquire GSS acceptor credential"); + + gss_release_buffer(&minor, &display_name); + gss_release_name(&minor, &target_name); + + return ret; +} + +krb5_error_code +_kdc_gss_rd_padata(astgs_request_t r, + const PA_DATA *pa, + gss_client_params **pgcp, + int *open) +{ + krb5_error_code ret; + + OM_uint32 minor; + gss_client_params *gcp = NULL; + gss_cred_id_t cred = GSS_C_NO_CREDENTIAL; + gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER; + struct gss_channel_bindings_struct cb; + + memset(&cb, 0, sizeof(cb)); + + *pgcp = NULL; + + if (!r->config->enable_gss_preauth) { + ret = KRB5KDC_ERR_POLICY; + goto out; + } + + if (pa->padata_value.length == 0) { + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto out; + } + + gcp = kdc_object_alloc(sizeof(*gcp), "pa-gss-client-params", pa_gss_dealloc_client_params); + if (gcp == NULL) { + ret = krb5_enomem(r->context); + goto out; + } + + /* errors are fast fail until gss_accept_sec_context() is called */ + gcp->major = GSS_S_NO_CONTEXT; + + ret = pa_gss_get_context_state(r, gcp); + if (ret) + goto out; + + ret = pa_gss_acquire_acceptor_cred(r, gcp, &cred); + if (ret) + goto out; + + _krb5_gss_data_to_buffer(&pa->padata_value, &input_token); + _krb5_gss_data_to_buffer(&r->req.req_body._save, &cb.application_data); + + gcp->major = gss_accept_sec_context(&gcp->minor, + &gcp->context_handle, + cred, + &input_token, + &cb, + &gcp->initiator_name, + &gcp->mech_type, + &gcp->output_token, + &gcp->flags, + &gcp->lifetime, + NULL); /* delegated_cred_handle */ + + ret = _krb5_gss_map_error(gcp->major, gcp->minor); + + if (GSS_ERROR(gcp->major)) { + pa_gss_display_status(r, gcp->major, gcp->minor, gcp, + "Failed to accept GSS security context"); + } else if ((gcp->flags & GSS_C_ANON_FLAG) && !_kdc_is_anon_request(&r->req)) { + kdc_log(r->context, r->config, 2, + "Anonymous GSS pre-authentication request w/o anonymous flag"); + ret = KRB5KDC_ERR_BADOPTION; + } else + *open = (gcp->major == GSS_S_COMPLETE); + +out: + gss_release_cred(&minor, &cred); + + if (gcp && gcp->major != GSS_S_NO_CONTEXT) + *pgcp = gcp; + else + kdc_object_release(gcp); + + return ret; +} + +krb5_timestamp +_kdc_gss_endtime(astgs_request_t r, + gss_client_params *gcp) +{ + krb5_timestamp endtime; + + if (gcp->lifetime == GSS_C_INDEFINITE) + endtime = 0; + else + endtime = kdc_time + gcp->lifetime; + + kdc_log(r->context, r->config, 10, + "GSS pre-authentication endtime is %ld", (long)endtime); + + return endtime; +} + +struct pa_gss_authorize_plugin_ctx { + astgs_request_t r; + struct gss_client_params *gcp; + krb5_boolean authorized; + krb5_principal initiator_princ; +}; + +static krb5_error_code KRB5_LIB_CALL +pa_gss_authorize_cb(krb5_context context, + const void *plug, + void *plugctx, + void *userctx) +{ + const krb5plugin_gss_preauth_authorizer_ftable *authorizer = plug; + struct pa_gss_authorize_plugin_ctx *pa_gss_authorize_plugin_ctx = userctx; + + return authorizer->authorize(plugctx, + pa_gss_authorize_plugin_ctx->r, + pa_gss_authorize_plugin_ctx->gcp->initiator_name, + pa_gss_authorize_plugin_ctx->gcp->mech_type, + pa_gss_authorize_plugin_ctx->gcp->flags, + &pa_gss_authorize_plugin_ctx->authorized, + &pa_gss_authorize_plugin_ctx->initiator_princ); +} + +static const char *plugin_deps[] = { + "kdc", + "hdb", + "gssapi", + "krb5", + NULL +}; + +static struct heim_plugin_data +gss_preauth_authorizer_data = { + "kdc", + KDC_GSS_PREAUTH_AUTHORIZER, + KDC_GSS_PREAUTH_AUTHORIZER_VERSION_1, + plugin_deps, + kdc_get_instance +}; + +static krb5_error_code +pa_gss_authorize_plugin(astgs_request_t r, + struct gss_client_params *gcp, + gss_const_buffer_t display_name, + krb5_boolean *authorized, + krb5_principal *initiator_princ) +{ + krb5_error_code ret; + struct pa_gss_authorize_plugin_ctx ctx; + + ctx.r = r; + ctx.gcp = gcp; + ctx.authorized = 0; + ctx.initiator_princ = NULL; + + krb5_clear_error_message(r->context); + ret = _krb5_plugin_run_f(r->context, &gss_preauth_authorizer_data, + 0, &ctx, pa_gss_authorize_cb); + + if (ret != KRB5_PLUGIN_NO_HANDLE) { + const char *msg = krb5_get_error_message(r->context, ret); + + kdc_log(r->context, r->config, 7, + "GSS authz plugin %sauthorize%s %s initiator %.*s: %s", + ctx.authorized ? "" : "did not " , + ctx.authorized ? "d" : "", + gss_oid_to_name(gcp->mech_type), + (int)display_name->length, (char *)display_name->value, + msg); + krb5_free_error_message(r->context, msg); + } + + *authorized = ctx.authorized; + *initiator_princ = ctx.initiator_princ; + + return ret; +} + +static krb5_error_code +pa_gss_authorize_default(astgs_request_t r, + struct gss_client_params *gcp, + gss_const_buffer_t display_name, + krb5_boolean *authorized, + krb5_principal *initiator_princ) +{ + krb5_error_code ret; + krb5_principal principal; + krb5_const_realm realm = r->server->principal->realm; + int flags = 0, cross_realm_allowed = 0, unauth_anon; + + /* + * gss_cross_realm_mechanisms_allowed is a list of GSS-API mechanisms + * that are allowed to map directly to Kerberos principals in any + * realm. If the authenticating mechanism is not on the list, then + * the initiator will be mapped to an enterprise principal in the + * service realm. This is useful to stop synthetic principals in + * foreign realms being conflated with true cross-realm principals. + */ + if (r->config->gss_cross_realm_mechanisms_allowed) { + OM_uint32 minor; + + gss_test_oid_set_member(&minor, gcp->mech_type, + r->config->gss_cross_realm_mechanisms_allowed, + &cross_realm_allowed); + } + + kdc_log(r->context, r->config, 10, + "Initiator %.*s will be mapped to %s", + (int)display_name->length, (char *)display_name->value, + cross_realm_allowed ? "nt-principal" : "nt-enterprise-principal"); + + if (!cross_realm_allowed) + flags |= KRB5_PRINCIPAL_PARSE_ENTERPRISE | KRB5_PRINCIPAL_PARSE_NO_REALM; + + ret = _krb5_gss_pa_parse_name(r->context, gcp->initiator_name, + flags, &principal); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + + kdc_log(r->context, r->config, 2, + "Failed to parse %s initiator name %.*s: %s", + gss_oid_to_name(gcp->mech_type), + (int)display_name->length, (char *)display_name->value, msg); + krb5_free_error_message(r->context, msg); + + return ret; + } + + /* + * GSS_C_ANON_FLAG indicates the client requested anonymous authentication + * (it is validated against the request-anonymous flag). + * + * _kdc_is_anonymous_pkinit() returns TRUE if the principal contains both + * the well known anonymous name and realm. + */ + unauth_anon = (gcp->flags & GSS_C_ANON_FLAG) && + _kdc_is_anonymous_pkinit(r->context, principal); + + /* + * Always use the anonymous entry created in our HDB, i.e. with the local + * realm, for authorizing anonymous requests. This matches PKINIT behavior + * as anonymous PKINIT requests include the KDC realm in the request. + */ + if (unauth_anon || (flags & KRB5_PRINCIPAL_PARSE_ENTERPRISE)) { + ret = krb5_principal_set_realm(r->context, principal, realm); + if (ret) { + krb5_free_principal(r->context, principal); + return ret; + } + } + + if (unauth_anon) { + /* + * Special case to avoid changing _kdc_as_rep(). If the initiator is + * the unauthenticated anonymous principal, r->client_princ also needs + * to be set in order to force the AS-REP realm to be set to the well- + * known anonymous identity. This is because (unlike anonymous PKINIT) + * we only require the anonymous flag, not the anonymous name, in the + * client AS-REQ. + */ + krb5_principal anon_princ; + + ret = krb5_copy_principal(r->context, principal, &anon_princ); + if (ret) + return ret; + + krb5_free_principal(r->context, r->client_princ); + r->client_princ = anon_princ; + } + + *authorized = TRUE; + *initiator_princ = principal; + + return 0; +} + +krb5_error_code +_kdc_gss_check_client(astgs_request_t r, + gss_client_params *gcp, + char **client_name) +{ + krb5_error_code ret; + krb5_principal initiator_princ = NULL; + hdb_entry *initiator = NULL; + krb5_boolean authorized = FALSE; + HDB *clientdb = r->clientdb; + + OM_uint32 minor; + gss_buffer_desc display_name = GSS_C_EMPTY_BUFFER; + gss_const_buffer_t display_name_p; + + *client_name = NULL; + + pa_gss_display_name(gcp->initiator_name, &display_name, &display_name_p); + + /* + * If no plugins handled the authorization request, then all clients + * are authorized as the directly corresponding Kerberos principal. + */ + ret = pa_gss_authorize_plugin(r, gcp, display_name_p, + &authorized, &initiator_princ); + if (ret == KRB5_PLUGIN_NO_HANDLE) + ret = pa_gss_authorize_default(r, gcp, display_name_p, + &authorized, &initiator_princ); + if (ret == 0 && !authorized) + ret = KRB5_KDC_ERR_CLIENT_NAME_MISMATCH; + if (ret) + goto out; + + ret = krb5_unparse_name(r->context, initiator_princ, client_name); + if (ret) + goto out; + + kdc_log(r->context, r->config, 4, + "Mapped GSS %s initiator %.*s to principal %s", + gss_oid_to_name(gcp->mech_type), + (int)display_name_p->length, (char *)display_name_p->value, + *client_name); + + ret = _kdc_db_fetch(r->context, + r->config, + initiator_princ, + HDB_F_FOR_AS_REQ | HDB_F_GET_CLIENT | + HDB_F_CANON | HDB_F_SYNTHETIC_OK, + NULL, + &r->clientdb, + &initiator); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + + kdc_log(r->context, r->config, 4, "UNKNOWN -- %s: %s", + *client_name, msg); + krb5_free_error_message(r->context, msg); + + goto out; + } + + /* + * If the AS-REQ client name was the well-known federated name, then + * replace the client name with the initiator name. Otherwise, the + * two principals must match, noting that GSS pre-authentication is + * for authentication, not general purpose impersonation. + */ + if (krb5_principal_is_federated(r->context, r->client->principal)) { + initiator->flags.force_canonicalize = 1; + + _kdc_free_ent(r->context, clientdb, r->client); + r->client = initiator; + initiator = NULL; + } else if (!krb5_principal_compare(r->context, + r->client->principal, + initiator->principal)) { + kdc_log(r->context, r->config, 2, + "GSS %s initiator %.*s does not match principal %s", + gss_oid_to_name(gcp->mech_type), + (int)display_name_p->length, (char *)display_name_p->value, + r->cname); + ret = KRB5_KDC_ERR_CLIENT_NAME_MISMATCH; + goto out; + } + +out: + krb5_free_principal(r->context, initiator_princ); + if (initiator) + _kdc_free_ent(r->context, r->clientdb, initiator); + gss_release_buffer(&minor, &display_name); + + return ret; +} + +krb5_error_code +_kdc_gss_mk_pa_reply(astgs_request_t r, + gss_client_params *gcp) +{ + krb5_error_code ret; + const KDC_REQ *req = &r->req; + + if (gcp->major == GSS_S_COMPLETE) { + krb5_enctype enctype; + uint32_t kfe = 0; + krb5_keyblock *reply_key = NULL; + + if (krb5_principal_is_krbtgt(r->context, r->server_princ)) + kfe |= KFE_IS_TGS; + + ret = _kdc_find_etype(r, kfe, req->req_body.etype.val, + req->req_body.etype.len, &enctype, NULL, NULL); + if (ret) + return ret; + + ret = _krb5_gss_pa_derive_key(r->context, gcp->context_handle, + req->req_body.nonce, + enctype, &reply_key); + if (ret) { + kdc_log(r->context, r->config, 10, + "Failed to derive GSS reply key: %d", ret); + return ret; + } + + krb5_free_keyblock_contents(r->context, &r->reply_key); + r->reply_key = *reply_key; + free(reply_key); + } else if (gcp->major == GSS_S_CONTINUE_NEEDED) { + ret = pa_gss_set_context_state(r, gcp); + if (ret) + return ret; + } + + /* only return padata in error case if we have an error token */ + if (!GSS_ERROR(gcp->major) || gcp->output_token.length) { + ret = krb5_padata_add(r->context, r->rep.padata, KRB5_PADATA_GSS, + gcp->output_token.value, gcp->output_token.length); + if (ret) + return ret; + + /* token is now owned by r->rep.padata */ + gcp->output_token.length = 0; + gcp->output_token.value = NULL; + } + + if (gcp->major == GSS_S_CONTINUE_NEEDED) + ret = KRB5_KDC_ERR_MORE_PREAUTH_DATA_REQUIRED; + else + ret = _krb5_gss_map_error(gcp->major, gcp->minor); + + return ret; +} + +krb5_error_code +_kdc_gss_mk_composite_name_ad(astgs_request_t r, + gss_client_params *gcp) +{ + krb5_error_code ret; + krb5_data data; + + OM_uint32 major, minor; + gss_buffer_desc namebuf = GSS_C_EMPTY_BUFFER; + + if (!r->config->enable_gss_auth_data || (gcp->flags & GSS_C_ANON_FLAG)) + return 0; + + major = gss_export_name_composite(&minor, gcp->initiator_name, &namebuf); + if (major == GSS_S_COMPLETE) { + _krb5_gss_buffer_to_data(&namebuf, &data); + + ret = _kdc_tkt_add_if_relevant_ad(r->context, &r->et, + KRB5_AUTHDATA_GSS_COMPOSITE_NAME, + &data); + } else if (major != GSS_S_UNAVAILABLE) + ret = _krb5_gss_map_error(major, minor); + else + ret = 0; + + gss_release_buffer(&minor, &namebuf); + + return ret; +} + +static void HEIM_CALLCONV +pa_gss_dealloc_client_params(void *ptr) +{ + gss_client_params *gcp = ptr; + OM_uint32 minor; + + if (gcp == NULL) + return; + + gss_delete_sec_context(&minor, &gcp->context_handle, GSS_C_NO_BUFFER); + gss_release_name(&minor, &gcp->initiator_name); + gss_release_buffer(&minor, &gcp->output_token); + free_Checksum(&gcp->req_body_checksum); + memset(gcp, 0, sizeof(*gcp)); +} + +krb5_error_code +_kdc_gss_get_mechanism_config(krb5_context context, + const char *section, + const char *key, + gss_OID_set *oidsp) +{ + krb5_error_code ret; + char **mechs, **mechp; + + gss_OID_set oids = GSS_C_NO_OID_SET; + OM_uint32 major, minor; + + mechs = krb5_config_get_strings(context, NULL, section, key, NULL); + if (mechs == NULL) + return 0; + + major = gss_create_empty_oid_set(&minor, &oids); + if (GSS_ERROR(major)) { + krb5_config_free_strings(mechs); + return _krb5_gss_map_error(major, minor); + } + + for (mechp = mechs; *mechp; mechp++) { + gss_OID oid = gss_name_to_oid(*mechp); + if (oid == GSS_C_NO_OID) + continue; + + major = gss_add_oid_set_member(&minor, oid, &oids); + if (GSS_ERROR(major)) + break; + } + + ret = _krb5_gss_map_error(major, minor); + if (ret == 0) + *oidsp = oids; + else + gss_release_oid_set(&minor, &oids); + + krb5_config_free_strings(mechs); + + return ret; +} + +static void +pa_gss_display_status(astgs_request_t r, + OM_uint32 major, + OM_uint32 minor, + gss_client_params *gcp, + const char *msg) +{ + krb5_error_code ret = _krb5_gss_map_error(major, minor); + gss_buffer_desc buf = GSS_C_EMPTY_BUFFER; + OM_uint32 dmaj, dmin; + OM_uint32 more = 0; + char *gmmsg = NULL; + char *gmsg = NULL; + char *s = NULL; + + do { + gss_release_buffer(&dmin, &buf); + dmaj = gss_display_status(&dmin, major, GSS_C_GSS_CODE, GSS_C_NO_OID, + &more, &buf); + if (GSS_ERROR(dmaj) || + buf.length >= INT_MAX || + asprintf(&s, "%s%s%.*s", gmsg ? gmsg : "", gmsg ? ": " : "", + (int)buf.length, (char *)buf.value) == -1 || + s == NULL) { + free(gmsg); + gmsg = NULL; + break; + } + gmsg = s; + s = NULL; + } while (!GSS_ERROR(dmaj) && more); + + if (gcp->mech_type != GSS_C_NO_OID) { + do { + gss_release_buffer(&dmin, &buf); + dmaj = gss_display_status(&dmin, major, GSS_C_MECH_CODE, + gcp->mech_type, &more, &buf); + if (GSS_ERROR(dmaj) || + asprintf(&s, "%s%s%.*s", gmmsg ? gmmsg : "", gmmsg ? ": " : "", + (int)buf.length, (char *)buf.value) == -1 || + s == NULL) { + free(gmmsg); + gmmsg = NULL; + break; + } + gmmsg = s; + s = NULL; + } while (!GSS_ERROR(dmaj) && more); + } + + if (gmsg == NULL) + krb5_set_error_message(r->context, ENOMEM, + "Error displaying GSS-API status"); + else + krb5_set_error_message(r->context, ret, "%s%s%s%s", gmsg, + gmmsg ? " (" : "", gmmsg ? gmmsg : "", + gmmsg ? ")" : ""); + krb5_prepend_error_message(r->context, ret, "%s", msg); + + kdc_log(r->context, r->config, 1, + "%s: %s%s%s%s", + msg, gmsg, gmmsg ? " (" : "", gmmsg ? gmmsg : "", + gmmsg ? ")" : ""); + + free(gmmsg); + free(gmsg); +} + +static const gss_buffer_desc +gss_pa_unknown_display_name = { + sizeof("<unknown name>") - 1, + "<unknown name>" +}; + +static void +pa_gss_display_name(gss_name_t name, + gss_buffer_t namebuf, + gss_const_buffer_t *namebuf_p) +{ + OM_uint32 major, minor; + + major = gss_display_name(&minor, name, namebuf, NULL); + if (GSS_ERROR(major)) + *namebuf_p = &gss_pa_unknown_display_name; + else + *namebuf_p = namebuf; +} + +static krb5_error_code KRB5_LIB_CALL +pa_gss_finalize_pac_cb(krb5_context context, + const void *plug, + void *plugctx, + void *userctx) +{ + const krb5plugin_gss_preauth_authorizer_ftable *authorizer = plug; + + return authorizer->finalize_pac(plugctx, userctx); +} + + +krb5_error_code +_kdc_gss_finalize_pac(astgs_request_t r, + gss_client_params *gcp) +{ + krb5_error_code ret; + + krb5_clear_error_message(r->context); + ret = _krb5_plugin_run_f(r->context, &gss_preauth_authorizer_data, + 0, r, pa_gss_finalize_pac_cb); + + if (ret == KRB5_PLUGIN_NO_HANDLE) + ret = 0; + + return ret; +} diff --git a/third_party/heimdal/kdc/gss_preauth_authorizer_plugin.h b/third_party/heimdal/kdc/gss_preauth_authorizer_plugin.h new file mode 100644 index 0000000..1bc1c91 --- /dev/null +++ b/third_party/heimdal/kdc/gss_preauth_authorizer_plugin.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019 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. + */ + +#ifndef HEIMDAL_KDC_GSS_PREAUTH_AUTHORIZER_PLUGIN_H +#define HEIMDAL_KDC_GSS_PREAUTH_AUTHORIZER_PLUGIN_H 1 + +#define KDC_GSS_PREAUTH_AUTHORIZER "kdc_gss_preauth_authorizer" +#define KDC_GSS_PREAUTH_AUTHORIZER_VERSION_1 1 + +#include <krb5.h> +#include <gssapi/gssapi.h> + +/* + * @param init Plugin initialization function (see krb5-plugin(7)) + * @param minor_version The plugin minor version number (1) + * @param fini Plugin finalization function + * @param authorize Plugin name authorization function + * + * -# plug_ctx, the context value output by the plugin's init function + * -# context, a krb5_context + * -# req, the AS-REQ request + * -# client_name, the requested client principal name + * -# client, the requested client HDB entry + * -# initiator_name, the authenticated GSS initiator name + * -# ret_flags, the flags returned by GSS_Init_sec_context() + * -# authorized, indicate whether the initiator was authorized + * -# mapped_name, the mapped principal name + * + * @ingroup krb5_support + */ + +typedef struct krb5plugin_gss_preauth_authorizer_ftable_desc { + HEIM_PLUGIN_FTABLE_COMMON_ELEMENTS(krb5_context); + krb5_error_code (KRB5_LIB_CALL *authorize)(void *, /*plug_ctx*/ + astgs_request_t, /*r*/ + gss_const_name_t, /*initiator_name*/ + gss_const_OID, /*mech_type*/ + OM_uint32, /*ret_flags*/ + krb5_boolean *, /*authorized*/ + krb5_principal *); /*mapped_name*/ + krb5_error_code (KRB5_LIB_CALL *finalize_pac)(void *, /*plug_ctx*/ + astgs_request_t); /*r*/ +} krb5plugin_gss_preauth_authorizer_ftable; + +#endif /* HEIMDAL_KDC_GSS_PREAUTH_AUTHORIZER_PLUGIN_H */ diff --git a/third_party/heimdal/kdc/headers.h b/third_party/heimdal/kdc/headers.h new file mode 100644 index 0000000..ffe49c8 --- /dev/null +++ b/third_party/heimdal/kdc/headers.h @@ -0,0 +1,118 @@ +/* + * Copyright (c) 1997 - 2006 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. + */ + +/* + * $Id$ + */ + +#ifndef __HEADERS_H__ +#define __HEADERS_H__ + +#include <config.h> + +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <signal.h> +#include <stdarg.h> +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#ifdef HAVE_SYS_TYPES_H +#include <sys/types.h> +#endif +#ifdef HAVE_FCNTL_H +#include <fcntl.h> +#endif +#ifdef HAVE_SYS_SELECT_H +#include <sys/select.h> +#endif +#ifdef HAVE_SYS_SOCKET_H +#include <sys/socket.h> +#endif +#ifdef HAVE_NETINET_IN_H +#include <netinet/in.h> +#endif +#ifdef HAVE_NETINET_IN6_H +#include <netinet/in6.h> +#endif +#ifdef HAVE_NETINET6_IN6_H +#include <netinet6/in6.h> +#endif +#ifdef HAVE_ARPA_INET_H +#include <arpa/inet.h> +#endif +#ifdef HAVE_SYS_WAIT_H +#include <sys/wait.h> +#endif +#ifdef HAVE_NETDB_H +#include <netdb.h> +#endif +#ifdef HAVE_UTIL_H +#include <util.h> +#endif +#ifdef HAVE_LIBUTIL_H +#include <libutil.h> +#endif +#include <err.h> +#include <roken.h> +#include <getarg.h> +#include <base64.h> +#include <parse_units.h> +#include <krb5.h> +#include <krb5_locl.h> +#include <digest_asn1.h> +#include <kx509_asn1.h> +#include <hdb.h> +#include <hdb_err.h> +#include <der.h> +#include <gssapi/gssapi.h> + +#ifndef NO_NTLM +#include <heimntlm.h> +#endif +#include <kdc.h> +#include <kdc-plugin.h> +#include <kdc-audit.h> + +#include <heimbase.h> + +#undef ALLOC +#define ALLOC(X) ((X) = calloc(1, sizeof(*(X)))) +#undef ALLOC_SEQ +#define ALLOC_SEQ(X, N) do { (X)->len = (N); \ +(X)->val = calloc((X)->len, sizeof(*(X)->val)); } while(0) + +#endif /* __HEADERS_H__ */ diff --git a/third_party/heimdal/kdc/hprop-version.rc b/third_party/heimdal/kdc/hprop-version.rc new file mode 100644 index 0000000..1e782f5 --- /dev/null +++ b/third_party/heimdal/kdc/hprop-version.rc @@ -0,0 +1,36 @@ +/*********************************************************************** + * Copyright (c) 2010, Secure Endpoints Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - 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. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 + * COPYRIGHT HOLDER 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 RC_FILE_TYPE VFT_APP +#define RC_FILE_DESC_0409 "KDC Database Propagation Tool" +#define RC_FILE_ORIG_0409 "hprop.exe" + +#include "../windows/version.rc" diff --git a/third_party/heimdal/kdc/hprop.8 b/third_party/heimdal/kdc/hprop.8 new file mode 100644 index 0000000..acf66bc --- /dev/null +++ b/third_party/heimdal/kdc/hprop.8 @@ -0,0 +1,130 @@ +.\" Copyright (c) 2000 - 2004 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. +.\" +.\" $Id$ +.\" +.Dd December 8, 2004 +.Dt HPROP 8 +.Os HEIMDAL +.Sh NAME +.Nm hprop +.Nd propagate the KDC database +.Sh SYNOPSIS +.Nm +.Bk -words +.Oo Fl m Ar file \*(Ba Xo +.Fl Fl master-key= Ns Pa file +.Xc +.Oc +.Oo Fl d Ar file \*(Ba Xo +.Fl Fl database= Ns Pa file +.Xc +.Oc +.Op Fl Fl source= Ns Ar heimdal|mit-dump +.Oo Fl r Ar string \*(Ba Xo +.Xc +.Oc +.Oo Fl c Ar cell \*(Ba Xo +.Fl Fl cell= Ns Ar cell +.Xc +.Oc +.Oo Fl k Ar keytab \*(Ba Xo +.Fl Fl keytab= Ns Ar keytab +.Xc +.Oc +.Oo Fl R Ar string \*(Ba Xo +.Fl Fl v5-realm= Ns Ar string +.Xc +.Oc +.Op Fl D | Fl Fl decrypt +.Op Fl E | Fl Fl encrypt +.Op Fl n | Fl Fl stdout +.Op Fl v | Fl Fl verbose +.Op Fl Fl version +.Op Fl h | Fl Fl help +.Op Ar host Ns Op : Ns Ar port +.Ar ... +.Ek +.Sh DESCRIPTION +.Nm +takes a principal database in a specified format and converts it into +a stream of Heimdal database records. This stream can either be +written to standard out, or (more commonly) be propagated to a +.Xr hpropd 8 +server running on a different machine. +.Pp +If propagating, it connects to all +.Ar hosts +specified on the command by opening a TCP connection to port 754 +(service hprop) and sends the database in encrypted form. +.Pp +Supported options: +.Bl -tag -width Ds +.It Fl m Ar file , Fl Fl master-key= Ns Pa file +Where to find the master key to encrypt or decrypt keys with. +.It Fl d Ar file , Fl Fl database= Ns Pa file +The database to be propagated. +.It Fl Fl source= Ns Ar heimdal|mit-dump|krb4-dump|kaserver +Specifies the type of the source database. Alternatives include: +.Pp +.Bl -tag -width mit-dump -compact -offset indent +.It heimdal +a Heimdal database +.It mit-dump +a MIT Kerberos 5 dump file +.El ++.It Fl k Ar keytab , Fl Fl keytab= Ns Ar keytab +The keytab to use for fetching the key to be used for authenticating +to the propagation daemon(s). The key +.Pa hprop/hostname +is used from this keytab. The default is to fetch the key from the +KDC database. +.It Fl R Ar string , Fl Fl v5-realm= Ns Ar string +Local realm override. +.It Fl D , Fl Fl decrypt +The encryption keys in the database can either be in clear, or +encrypted with a master key. This option transmits the database with +unencrypted keys. +.It Fl E , Fl Fl encrypt +This option transmits the database with encrypted keys. This is the +default if no option is supplied. +.It Fl n , Fl Fl stdout +Dump the database on stdout, in a format that can be fed to hpropd. +.El +.Sh EXAMPLES +The following will propagate a database to another machine (which +should run +.Xr hpropd 8 ) : +.Bd -literal -offset indent +$ hprop slave-1 slave-2 +.Ed +.Sh SEE ALSO +.Xr hpropd 8 diff --git a/third_party/heimdal/kdc/hprop.c b/third_party/heimdal/kdc/hprop.c new file mode 100644 index 0000000..c1db11b --- /dev/null +++ b/third_party/heimdal/kdc/hprop.c @@ -0,0 +1,482 @@ +/* + * Copyright (c) 1997 - 2005 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 KRB5_DEPRECATED /* uses v4 functions that will die */ + +#include "hprop.h" + +static int version_flag; +static int help_flag; +static const char *ktname = HPROP_KEYTAB; +static const char *database; +static char *mkeyfile; +static int to_stdout; +static int verbose_flag; +static int encrypt_flag; +static int decrypt_flag; +static hdb_master_key mkey5; + +static char *source_type; + +static char *local_realm=NULL; + +static int +open_socket(krb5_context context, const char *hostname, const char *port) +{ + struct addrinfo *ai, *a; + struct addrinfo hints; + int error; + + memset (&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + error = getaddrinfo (hostname, port, &hints, &ai); + if (error) { + warnx ("%s: %s", hostname, gai_strerror(error)); + return -1; + } + + for (a = ai; a != NULL; a = a->ai_next) { + int s; + + s = socket (a->ai_family, a->ai_socktype, a->ai_protocol); + if (s < 0) + continue; + if (connect (s, a->ai_addr, a->ai_addrlen) < 0) { + warn ("connect(%s)", hostname); + close (s); + continue; + } + freeaddrinfo (ai); + return s; + } + warnx ("failed to contact %s", hostname); + freeaddrinfo (ai); + return -1; +} + +krb5_error_code +v5_prop(krb5_context context, HDB *db, hdb_entry *entry, void *appdata) +{ + krb5_error_code ret; + struct prop_data *pd = appdata; + krb5_data data; + + if(encrypt_flag) { + ret = hdb_seal_keys_mkey(context, entry, mkey5); + if (ret) { + krb5_warn(context, ret, "hdb_seal_keys_mkey"); + return ret; + } + } + if(decrypt_flag) { + ret = hdb_unseal_keys_mkey(context, entry, mkey5); + if (ret) { + krb5_warn(context, ret, "hdb_unseal_keys_mkey"); + return ret; + } + } + + ret = hdb_entry2value(context, entry, &data); + if(ret) { + krb5_warn(context, ret, "hdb_entry2value"); + return ret; + } + + if(to_stdout) + ret = krb5_write_message(context, &pd->sock, &data); + else + ret = krb5_write_priv_message(context, pd->auth_context, + &pd->sock, &data); + krb5_data_free(&data); + return ret; +} + +struct getargs args[] = { + { "master-key", 'm', arg_string, &mkeyfile, "v5 master key file", "file" }, + { "database", 'd', arg_string, rk_UNCONST(&database), "database", "file" }, + { "source", 0, arg_string, &source_type, "type of database to read", + "heimdal" + "|mit-dump" + }, + + { "keytab", 'k', arg_string, rk_UNCONST(&ktname), + "keytab to use for authentication", "keytab" }, + { "v5-realm", 'R', arg_string, &local_realm, "v5 realm to use", NULL }, + { "decrypt", 'D', arg_flag, &decrypt_flag, "decrypt keys", NULL }, + { "encrypt", 'E', arg_flag, &encrypt_flag, "encrypt keys", NULL }, + { "stdout", 'n', arg_flag, &to_stdout, "dump to stdout", NULL }, + { "verbose", 'v', arg_flag, &verbose_flag, NULL, NULL }, + { "version", 0, arg_flag, &version_flag, NULL, NULL }, + { "help", 'h', arg_flag, &help_flag, NULL, NULL } +}; + +static int num_args = sizeof(args) / sizeof(args[0]); + +static void +usage(int ret) +{ + arg_printusage (args, num_args, NULL, "[host[:port]] ..."); + exit (ret); +} + +static void +get_creds(krb5_context context, krb5_ccache *cache) +{ + krb5_keytab keytab; + krb5_principal client; + krb5_error_code ret; + krb5_get_init_creds_opt *init_opts; + krb5_preauthtype preauth = KRB5_PADATA_ENC_TIMESTAMP; + krb5_creds creds; + + ret = krb5_kt_register(context, &hdb_get_kt_ops); + if(ret) krb5_err(context, 1, ret, "krb5_kt_register"); + + ret = krb5_kt_resolve(context, ktname, &keytab); + if(ret) krb5_err(context, 1, ret, "krb5_kt_resolve"); + + ret = krb5_make_principal(context, &client, NULL, + "kadmin", HPROP_NAME, NULL); + if(ret) krb5_err(context, 1, ret, "krb5_make_principal"); + + ret = krb5_get_init_creds_opt_alloc(context, &init_opts); + if(ret) krb5_err(context, 1, ret, "krb5_get_init_creds_opt_alloc"); + krb5_get_init_creds_opt_set_preauth_list(init_opts, &preauth, 1); + + ret = krb5_get_init_creds_keytab(context, &creds, client, keytab, 0, NULL, init_opts); + if(ret) krb5_err(context, 1, ret, "krb5_get_init_creds"); + + krb5_get_init_creds_opt_free(context, init_opts); + + ret = krb5_kt_close(context, keytab); + if(ret) krb5_err(context, 1, ret, "krb5_kt_close"); + + ret = krb5_cc_new_unique(context, krb5_cc_type_memory, NULL, cache); + if(ret) krb5_err(context, 1, ret, "krb5_cc_new_unique"); + + ret = krb5_cc_initialize(context, *cache, client); + if(ret) krb5_err(context, 1, ret, "krb5_cc_initialize"); + + krb5_free_principal(context, client); + + ret = krb5_cc_store_cred(context, *cache, &creds); + if(ret) krb5_err(context, 1, ret, "krb5_cc_store_cred"); + + krb5_free_cred_contents(context, &creds); +} + +enum hprop_source { + HPROP_HEIMDAL = 1, + HPROP_MIT_DUMP +}; + +struct { + int type; + const char *name; +} types[] = { + { HPROP_HEIMDAL, "heimdal" }, + { HPROP_MIT_DUMP, "mit-dump" } +}; + +static int +parse_source_type(const char *s) +{ + size_t i; + for(i = 0; i < sizeof(types) / sizeof(types[0]); i++) { + if(strstr(types[i].name, s) == types[i].name) + return types[i].type; + } + return 0; +} + +static int +iterate (krb5_context context, + const char *database_name, + HDB *db, + int type, + struct prop_data *pd) +{ + int ret; + + switch(type) { + case HPROP_MIT_DUMP: + ret = mit_prop_dump(pd, database_name); + if (ret) + krb5_warn(context, ret, "mit_prop_dump"); + break; + case HPROP_HEIMDAL: + ret = hdb_foreach(context, db, HDB_F_DECRYPT, v5_prop, pd); + if(ret) + krb5_warn(context, ret, "hdb_foreach"); + break; + default: + krb5_errx(context, 1, "unknown prop type: %d", type); + } + return ret; +} + +static int +dump_database (krb5_context context, int type, + const char *database_name, HDB *db) +{ + krb5_error_code ret; + struct prop_data pd; + krb5_data data; + + pd.context = context; + pd.auth_context = NULL; + pd.sock = STDOUT_FILENO; + + ret = iterate (context, database_name, db, type, &pd); + if (ret) + krb5_errx(context, 1, "iterate failure"); + krb5_data_zero (&data); + ret = krb5_write_message (context, &pd.sock, &data); + if (ret) + krb5_err(context, 1, ret, "krb5_write_message"); + + return 0; +} + +static int +propagate_database (krb5_context context, int type, + const char *database_name, + HDB *db, krb5_ccache ccache, + int optidx, int argc, char **argv) +{ + krb5_principal server; + krb5_error_code ret; + int i, failed = 0; + + for(i = optidx; i < argc; i++){ + krb5_auth_context auth_context; + int fd; + struct prop_data pd; + krb5_data data; + + char *port, portstr[NI_MAXSERV]; + char *host = argv[i]; + + port = strchr(host, ':'); + if(port == NULL) { + snprintf(portstr, sizeof(portstr), "%u", + ntohs(krb5_getportbyname (context, "hprop", "tcp", + HPROP_PORT))); + port = portstr; + } else + *port++ = '\0'; + + fd = open_socket(context, host, port); + if(fd < 0) { + failed++; + krb5_warn (context, errno, "connect %s", host); + continue; + } + + ret = krb5_sname_to_principal(context, argv[i], + HPROP_NAME, KRB5_NT_SRV_HST, &server); + if(ret) { + failed++; + krb5_warn(context, ret, "krb5_sname_to_principal(%s)", host); + close(fd); + continue; + } + + if (local_realm) { + krb5_realm my_realm; + ret = krb5_get_default_realm(context,&my_realm); + if (ret == 0) { + ret = krb5_principal_set_realm(context,server,my_realm); + krb5_xfree(my_realm); + } + if (ret) { + failed++; + krb5_warn(context, ret, "unable to obtain default or set realm"); + krb5_free_principal(context, server); + close(fd); + continue; + } + } + + auth_context = NULL; + ret = krb5_sendauth(context, + &auth_context, + &fd, + HPROP_VERSION, + NULL, + server, + AP_OPTS_MUTUAL_REQUIRED | AP_OPTS_USE_SUBKEY, + NULL, /* in_data */ + NULL, /* in_creds */ + ccache, + NULL, + NULL, + NULL); + + krb5_free_principal(context, server); + + if(ret) { + failed++; + krb5_warn(context, ret, "krb5_sendauth (%s)", host); + goto next_host; + } + + pd.context = context; + pd.auth_context = auth_context; + pd.sock = fd; + + ret = iterate (context, database_name, db, type, &pd); + if (ret) { + krb5_warnx(context, "iterate to host %s failed", host); + failed++; + goto next_host; + } + + krb5_data_zero (&data); + ret = krb5_write_priv_message(context, auth_context, &fd, &data); + if(ret) { + krb5_warn(context, ret, "krb5_write_priv_message"); + failed++; + goto next_host; + } + + ret = krb5_read_priv_message(context, auth_context, &fd, &data); + if(ret) { + krb5_warn(context, ret, "krb5_read_priv_message: %s", host); + failed++; + goto next_host; + } else + krb5_data_free (&data); + + next_host: + krb5_auth_con_free(context, auth_context); + close(fd); + } + if (failed) + return 1; + return 0; +} + +int +main(int argc, char **argv) +{ + krb5_error_code ret; + krb5_context context; + krb5_ccache ccache = NULL; + HDB *db = NULL; + int optidx = 0; + + int type, exit_code; + + setprogname(argv[0]); + + if(getarg(args, num_args, argc, argv, &optidx)) + usage(1); + + if(help_flag) + usage(0); + + if(version_flag){ + print_version(NULL); + exit(0); + } + + ret = krb5_init_context(&context); + if(ret) + exit(1); + + /* We may be reading an old database encrypted with a DES master key. */ + ret = krb5_allow_weak_crypto(context, 1); + if(ret) + krb5_err(context, 1, ret, "krb5_allow_weak_crypto"); + + if(local_realm) + krb5_set_default_realm(context, local_realm); + + if(encrypt_flag && decrypt_flag) + krb5_errx(context, 1, + "only one of `--encrypt' and `--decrypt' is meaningful"); + + if(source_type != NULL) { + type = parse_source_type(source_type); + if(type == 0) + krb5_errx(context, 1, "unknown source type `%s'", source_type); + } else + type = HPROP_HEIMDAL; + + if(!to_stdout) + get_creds(context, &ccache); + + if(decrypt_flag || encrypt_flag) { + ret = hdb_read_master_key(context, mkeyfile, &mkey5); + if(ret && ret != ENOENT) + krb5_err(context, 1, ret, "hdb_read_master_key"); + if(ret) + krb5_errx(context, 1, "No master key file found"); + } + + switch(type) { + case HPROP_MIT_DUMP: + if (database == NULL) + krb5_errx(context, 1, "no dump file specified"); + break; + case HPROP_HEIMDAL: + ret = hdb_create (context, &db, database); + if(ret) + krb5_err(context, 1, ret, "hdb_create: %s", database); + ret = db->hdb_open(context, db, O_RDONLY, 0); + if(ret) + krb5_err(context, 1, ret, "db->hdb_open"); + break; + default: + krb5_errx(context, 1, "unknown dump type `%d'", type); + break; + } + + if (to_stdout) + exit_code = dump_database (context, type, database, db); + else + exit_code = propagate_database (context, type, database, + db, ccache, optidx, argc, argv); + + if(ccache != NULL) + krb5_cc_destroy(context, ccache); + + if(db != NULL) + (*db->hdb_destroy)(context, db); + + krb5_free_context(context); + return exit_code; +} diff --git a/third_party/heimdal/kdc/hprop.h b/third_party/heimdal/kdc/hprop.h new file mode 100644 index 0000000..59c39ea --- /dev/null +++ b/third_party/heimdal/kdc/hprop.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 1997 - 2000 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. + */ + +/* $Id$ */ + +#ifndef __HPROP_H__ +#define __HPROP_H__ + +#include "headers.h" + +struct prop_data{ + krb5_context context; + krb5_auth_context auth_context; + int sock; +}; + +#define HPROP_VERSION "hprop-0.0" +#define HPROP_NAME "hprop" +#define HPROP_KEYTAB "HDBGET:" +#define HPROP_PORT 754 + +#ifndef NEVERDATE +#define NEVERDATE ((1U << 31) - 1) +#endif + +krb5_error_code v5_prop(krb5_context, HDB*, hdb_entry*, void*); +int mit_prop_dump(void*, const char*); + +#endif /* __HPROP_H__ */ diff --git a/third_party/heimdal/kdc/hpropd-version.rc b/third_party/heimdal/kdc/hpropd-version.rc new file mode 100644 index 0000000..388d64d --- /dev/null +++ b/third_party/heimdal/kdc/hpropd-version.rc @@ -0,0 +1,36 @@ +/*********************************************************************** + * Copyright (c) 2010, Secure Endpoints Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - 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. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 + * COPYRIGHT HOLDER 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 RC_FILE_TYPE VFT_APP +#define RC_FILE_DESC_0409 "Propagated KDC database recipient" +#define RC_FILE_ORIG_0409 "hpropd.exe" + +#include "../windows/version.rc" diff --git a/third_party/heimdal/kdc/hpropd.8 b/third_party/heimdal/kdc/hpropd.8 new file mode 100644 index 0000000..9056b05 --- /dev/null +++ b/third_party/heimdal/kdc/hpropd.8 @@ -0,0 +1,87 @@ +.\" Copyright (c) 1997, 2000 - 2003 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. +.\" +.\" $Id$ +.\" +.Dd August 27, 1997 +.Dt HPROPD 8 +.Os HEIMDAL +.Sh NAME +.Nm hpropd +.Nd receive a propagated database +.Sh SYNOPSIS +.Nm +.Bk -words +.Oo Fl d Ar file \*(Ba Xo +.Fl Fl database= Ns Ar file +.Xc +.Oc +.Op Fl n | Fl Fl stdin +.Op Fl Fl print +.Op Fl i | Fl Fl no-inetd +.Oo Fl k Ar keytab \*(Ba Xo +.Fl Fl keytab= Ns Ar keytab +.Xc +.Oc +.Ek +.Sh DESCRIPTION +.Nm +receives a database sent by +.Nm hprop . +and writes it as a local database. +.Pp +By default, +.Nm +expects to be started from +.Nm inetd +if stdin is a socket and expects to receive the dumped database over +stdin otherwise. +If the database is sent over the network, it is authenticated and +encrypted. +Only connections authenticated with the principal +.Nm kadmin Ns / Ns Nm hprop +are accepted. +.Pp +Options supported: +.Bl -tag -width Ds +.It Fl d Ar file , Fl Fl database= Ns Ar file +database +.It Fl n , Fl Fl stdin +read from stdin +.It Fl Fl print +print dump to stdout +.It Fl i , Fl Fl no-inetd +not started from inetd +.It Fl k Ar keytab , Fl Fl keytab= Ns Ar keytab +keytab to use for authentication +.El +.Sh SEE ALSO +.Xr hprop 8 diff --git a/third_party/heimdal/kdc/hpropd.c b/third_party/heimdal/kdc/hpropd.c new file mode 100644 index 0000000..255d609 --- /dev/null +++ b/third_party/heimdal/kdc/hpropd.c @@ -0,0 +1,285 @@ +/* + * Copyright (c) 1997-2006 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. + */ + +#include "hprop.h" + +static int inetd_flag = -1; +static int help_flag; +static int version_flag; +static int print_dump; +static const char *database; +static int from_stdin; +static char *local_realm; +static char *ktname = NULL; + +struct getargs args[] = { + { "database", 'd', arg_string, rk_UNCONST(&database), "database", "file" }, + { "stdin", 'n', arg_flag, &from_stdin, "read from stdin", NULL }, + { "print", 0, arg_flag, &print_dump, "print dump to stdout", NULL }, +#ifdef SUPPORT_INETD + { "inetd", 'i', arg_negative_flag, &inetd_flag, + "Not started from inetd", NULL }, +#endif + { "keytab", 'k', arg_string, &ktname, "keytab to use for authentication", "keytab" }, + { "realm", 'r', arg_string, &local_realm, "realm to use", NULL }, + { "version", 0, arg_flag, &version_flag, NULL, NULL }, + { "help", 'h', arg_flag, &help_flag, NULL, NULL} +}; + +static int num_args = sizeof(args) / sizeof(args[0]); +static char unparseable_name[] = "unparseable name"; + +static void +usage(int ret) +{ + arg_printusage (args, num_args, NULL, ""); + exit (ret); +} + +int +main(int argc, char **argv) +{ + krb5_error_code ret; + krb5_context context; + krb5_auth_context ac = NULL; + krb5_principal c1, c2; + krb5_authenticator authent; + krb5_keytab keytab; + krb5_socket_t sock = rk_INVALID_SOCKET; + HDB *db = NULL; + int optidx = 0; + char *tmp_db = NULL; + krb5_log_facility *fac; + int nprincs; + + setprogname(argv[0]); + + ret = krb5_init_context(&context); + if (ret) + exit(1); + + ret = krb5_openlog(context, "hpropd", &fac); + if (ret) + errx(1, "krb5_openlog"); + krb5_set_warn_dest(context, fac); + + if (getarg(args, num_args, argc, argv, &optidx)) + usage(1); + + if (local_realm != NULL) + krb5_set_default_realm(context, local_realm); + + if (help_flag) + usage(0); + if (version_flag) { + print_version(NULL); + exit(0); + } + + argc -= optidx; + argv += optidx; + + if (argc != 0) + usage(1); + + if (database == NULL) + database = hdb_default_db(context); + + if (from_stdin) { + sock = STDIN_FILENO; + } else { + struct sockaddr_storage ss; + struct sockaddr *sa = (struct sockaddr *)&ss; + socklen_t sin_len = sizeof(ss); + char addr_name[256]; + krb5_ticket *ticket; + char *server; + + memset(&ss, 0, sizeof(ss)); + sock = STDIN_FILENO; +#ifdef SUPPORT_INETD + if (inetd_flag == -1) { + if (getpeername (sock, sa, &sin_len) < 0) { + inetd_flag = 0; + } else { + inetd_flag = 1; + } + } +#else + inetd_flag = 0; +#endif + if (!inetd_flag) { + mini_inetd (krb5_getportbyname (context, "hprop", "tcp", + HPROP_PORT), &sock); + } + socket_set_keepalive(sock, 1); + sin_len = sizeof(ss); + if (getpeername(sock, sa, &sin_len) < 0) + krb5_err(context, 1, errno, "getpeername"); + + if (inet_ntop(sa->sa_family, + socket_get_address (sa), + addr_name, + sizeof(addr_name)) == NULL) + strlcpy (addr_name, "unknown address", + sizeof(addr_name)); + + krb5_log(context, fac, 0, "Connection from %s", addr_name); + + ret = krb5_kt_register(context, &hdb_get_kt_ops); + if (ret) + krb5_err(context, 1, ret, "krb5_kt_register"); + + if (ktname != NULL) { + ret = krb5_kt_resolve(context, ktname, &keytab); + if (ret) + krb5_err (context, 1, ret, "krb5_kt_resolve %s", ktname); + } else { + ret = krb5_kt_default (context, &keytab); + if (ret) + krb5_err (context, 1, ret, "krb5_kt_default"); + } + + ret = krb5_recvauth(context, &ac, &sock, HPROP_VERSION, NULL, + 0, keytab, &ticket); + if (ret) + krb5_err(context, 1, ret, "krb5_recvauth"); + + ret = krb5_unparse_name(context, ticket->server, &server); + if (ret) + krb5_err(context, 1, ret, "krb5_unparse_name"); + if (strncmp(server, "hprop/", 6) != 0) + krb5_errx(context, 1, "ticket not for hprop (%s)", server); + + free(server); + krb5_free_ticket (context, ticket); + + ret = krb5_auth_con_getauthenticator(context, ac, &authent); + if (ret) + krb5_err(context, 1, ret, "krb5_auth_con_getauthenticator"); + + ret = krb5_make_principal(context, &c1, NULL, "kadmin", "hprop", NULL); + if (ret) + krb5_err(context, 1, ret, "krb5_make_principal"); + _krb5_principalname2krb5_principal(context, &c2, + authent->cname, authent->crealm); + if (!krb5_principal_compare(context, c1, c2)) { + char *s; + ret = krb5_unparse_name(context, c2, &s); + if (ret) + s = unparseable_name; + krb5_errx(context, 1, "Unauthorized connection from %s", s); + } + krb5_free_principal(context, c1); + krb5_free_principal(context, c2); + + ret = krb5_kt_close(context, keytab); + if (ret) + krb5_err(context, 1, ret, "krb5_kt_close"); + } + + if (asprintf(&tmp_db, "%s~", database) < 0 || tmp_db == NULL) + krb5_errx(context, 1, "hdb_create: out of memory"); + + ret = hdb_create(context, &db, tmp_db); + if (ret) + krb5_err(context, 1, ret, "hdb_create(%s)", tmp_db); + ret = db->hdb_open(context, db, O_RDWR | O_CREAT | O_TRUNC, 0600); + if (ret) + krb5_err(context, 1, ret, "hdb_open(%s)", tmp_db); + + nprincs = 0; + while (1){ + krb5_data data; + hdb_entry entry; + + if (from_stdin) { + ret = krb5_read_message(context, &sock, &data); + if (ret != 0 && ret != HEIM_ERR_EOF) + krb5_err(context, 1, ret, "krb5_read_message"); + } else { + ret = krb5_read_priv_message(context, ac, &sock, &data); + if (ret) + krb5_err(context, 1, ret, "krb5_read_priv_message"); + } + + if (ret == HEIM_ERR_EOF || data.length == 0) { + if (!from_stdin) { + data.data = NULL; + data.length = 0; + krb5_write_priv_message(context, ac, &sock, &data); + } + break; + } + memset(&entry, 0, sizeof(entry)); + ret = hdb_value2entry(context, &data, &entry); + krb5_data_free(&data); + if (ret) + krb5_err(context, 1, ret, "hdb_value2entry"); + if (print_dump) { + struct hdb_print_entry_arg parg; + + parg.out = stdout; + parg.fmt = HDB_DUMP_HEIMDAL; + hdb_print_entry(context, db, &entry, &parg); + } else { + ret = db->hdb_store(context, db, 0, &entry); + if (ret == HDB_ERR_EXISTS) { + char *s; + ret = krb5_unparse_name(context, entry.principal, &s); + if (ret) + s = strdup(unparseable_name); + krb5_warnx(context, "Entry exists: %s", s); + free(s); + } else if (ret) + krb5_err(context, 1, ret, "db_store"); + else + nprincs++; + } + hdb_free_entry(context, db, &entry); + } + if (!print_dump) + krb5_log(context, fac, 0, "Received %d principals", nprincs); + + ret = db->hdb_close(context, db); + if (ret) + krb5_err(context, 1, ret, "db_close"); + ret = db->hdb_rename(context, db, database); + if (ret) + krb5_err(context, 1, ret, "db_rename"); + + if (inetd_flag == 0) + rk_closesocket(sock); + + exit(0); +} diff --git a/third_party/heimdal/kdc/httpkadmind.8 b/third_party/heimdal/kdc/httpkadmind.8 new file mode 100644 index 0000000..345e959 --- /dev/null +++ b/third_party/heimdal/kdc/httpkadmind.8 @@ -0,0 +1,653 @@ +.\" 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 allow-GET +.Op Fl Fl no-allow-GET +.Op Fl Fl GET-with-csrf-token +.Op Fl Fl csrf-header= Ns Ar HEADER-NAME +.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 over HTTP: +.Ar /get-keys and +.Ar /get-config . +.Pp +The +.Ar /get-keys +end-point allows callers to get a principal's keys in +.Dq keytab +format for named principals, possibly performing write operations +such as creating a non-existent principal, or rotating its keys, +if requested. +Note that this end-point can cause KDC HDB principal entries to +be modified or created incidental to fetching the principal's +keys. +The use of the HTTP POST method is required when this end-point +writes to the KDC's HDB. +See below. +.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 +.Sh HTTP APIS +All HTTP APIs served by this program accept POSTs, with all +request parameters given as either URI query parameters, and/or +as form data in the POST request body, in either +.Ar application/x-www-form-urlencoded +or +.Ar multipart/formdata . +If GETs are enabled, then request parameters must be supplied as +URI query parameters. +.Pp +Note that requests that cause changes to the HDB must always be +done via POST, never GET. +.Pp +URI query parameters must be of the form +.Ar param0=value¶m1=value... +Some parameters can be given multiple values -- see the +descriptions of the end-points. +.Sh CROSS-SITE REQUEST FORGERY PROTECTION +.Em None +of the resources service by this service are intended to be +executed by web pages. +.Pp +Most of the resources provided by this service are +.Dq safe +in the sense that they do not change server-side state besides +logging, and in that they are idempotent, but they are +only safe to execute +.Em if and only if +the requesting party is trusted to see the response. +Since none of these resources are intended to be used from web +pages, it is important that web pages not be able to execute them +.Em and +observe the responses. +.Pp +Some of the resources provided by this service do change +server-side state, specifically principal entries in the KDC's +HDB. +Those always require the use of POST, not GET. +.Pp +In a web browser context, pages from other origins will be able +to attempt requests to this service, but should never be able to +see the responses because browsers normally wouldn't allow that. +Nonetheless, anti cross site request forgery (CSRF) protection +may be desirable. +.Pp +This service provides the following CSRF protection features: +.Bl -tag -width Ds -offset indent +.It requests are rejected if they have a +.Dq Referer +(except the experimental /get-negotiate-token end-point) +.It the service can be configured to require a header that would make the +request not Dq simple +.It GETs can be disabled (see options), thus requiring POSTs +.It GETs can be required to have a CSRF token (see below) +.It POSTs can be required to have a CSRF token +.El +.Pp +The experimental +.Ar /get-negotiate-token +end-point, however, always accepts +.Dq Referer +requests. +.Pp +To obtain a CSRF token, first execute the request without the +CSRF token, and the resulting error +response will include a +.Ar X-CSRF-Token +response header. +.Pp +To execute a request with a CSRF token, first obtain a CSRF token +as described above, then copy the token to the request as the +value of the request's +.Ar X-CSRF-Token +header. +.Pp +The key for keying the CSRF token HMAC is that of the first +current key for the +.Sq WELLKNOWN/CSRFTOKEN +principal for the realm being used. +Every realm served by this service must have this principal. +.Sh GETTING KEYTABS +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. +The default attributes for new principals created this way will +be taken (except for the disabled attribute) from any containing +virtual host-based service principal namespace that include a +leading +.Sq . +in the hostname component, or from +.Nm krb5.conf(5) +(see the CONFIGURATION section). +.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 +The HTTP +.Nm Cache-Control +header will be set on +.Nm get-keys +responses to +.Dq Nm no-store , +and the +.Nm max-age +cache control parameter will be set to the least number of +seconds until before any of the requested principal's keys could +change. +For virtual principals this will be either the time left until a +quarter of the rotation period before the next rotation, or the +time left until a +quarter of the rotation period after the next rotation. +For concrete principals this will be the time left to the first +such principal's password expiration, or, if none of them have a +configured password expiration time, then half of the +.Nm new_service_key_delay +configured in the +.Nm [hdb] +section of the +.Nm krb5.conf(5) +file. +.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. +Clients with host-based principals for the +.Dq host +service can create and extract keys for their own service name, +but otherwise a number of service names are denied: +.Bl -tag -width Ds -offset indent +.It Dq host +.It Dq root +.It Dq exceed +.El +as well as all the service names for Heimdal-specific services: +.Bl -tag -width Ds -offset indent +.It Dq krbtgt +.It Dq iprop +.It Dq kadmin +.It Dq hprop +.It Dq WELLKNOWN +.It Dq K +.El +.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 allow-GET +.Xc +If given, then HTTP GET will be allowed for the various end-points +other than +.Ar /health . +Otherwise only HEAD and POST will be allowed. +By default GETs are allowed, but this will change soon. +.It Xo +.Fl Fl no-allow-GET +.Xc +If given then HTTP GETs will be rejected for the various +end-points other than +.Ar /health . +.It Xo +.Fl Fl csrf-protection-type= Ns Ar CSRF-PROTECTION-TYPE +.Xc +Possible values of +.Ar CSRF-PROTECTION-TYPE +are +.Bl -bullet -compact -offset indent +.It +.Li GET-with-header +.It +.Li GET-with-token +.It +.Li POST-with-header +.It +.Li POST-with-token +.El +This may be given multiple times. +The default is to require CSRF tokens for POST requests, and to +require neither a non-simple header nor a CSRF token for GET +requests. +.Pp +See +.Sx CROSS-SITE REQUEST FORGERY PROTECTION . +.It Xo +.Fl Fl csrf-header= Ns Ar HEADER-NAME +.Xc +If given, then all requests other than to the +.Ar /health +service must have the given request +.Ar HEADER-NAME +set (the value is irrelevant). +.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.<realm> . +.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.<realm> . +.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 +.Bd -literal -offset indent +[ext_keytab] + simple_csr_authorizer_directory = /etc/krb5/simple_csr_authz + ipc_csr_authorizer = { + service = UNIX:/var/heimdal/csr_authorizer_sock + } +.Ed +.Pp +Configuration parameters specific to +.Nm httpkadmind : +.Bl -tag -width Ds -offset indent +.It csr_authorizer_handles_svc_names = BOOL +.It new_hostbased_service_principal_attributes = ... +.El +.Pp +The +.Nm [ext_keytab] +.Nm get_keys_max_spns = NUMBER +parameter can be used to specify a maximum number of principals whose +keys can be retrieved in one +.Nm GET +of the +.Nm /get-keys +end-point. +Defaults to 400. +.Pp +The +.Nm [ext_keytab] +.Nm new_hostbased_service_principal_attributes +parameter may be used instead of virtual host-based service +namespace principals to specify the attributes of new principals +created by +.Nm httpkadmind , +and its value is a hive with a service name then a hostname or +namespace, and whose value is a set of attributes as given in the +.Nm kadmin(1) modify +command. +For example: +.Bd -literal -offset indent +[ext_keytab] + new_hostbased_service_principal_attributes = { + host = { + a-particular-hostname.test.h5l.se = ok-as-delegate + .prod.test.h5l.se = ok-as-delegate + } + } +.Ed +.Pp +which means that +.Dq host/a-particular-hostname.test.h5l.se , +if created via +.Nm httpkadmind , +will be allowed to get delegated credentials (ticket forwarding), +and that hostnames matching the glob pattern +.Dq host/*.prod.test.h5l.se , +if created via +.Nm httpkadmind , +will also allowed to get delegated credentials. +All host-based service principals created via +.Nm httpkadmind +not matchining any +.Nm new_hostbased_service_principal_attributes +service namespaces will have the empty attribute set. +.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/third_party/heimdal/kdc/httpkadmind.c b/third_party/heimdal/kdc/httpkadmind.c new file mode 100644 index 0000000..1d64e84 --- /dev/null +++ b/third_party/heimdal/kdc/httpkadmind.c @@ -0,0 +1,2762 @@ +/* + * Copyright (c) 2020 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the Institute nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + */ + +#define _XOPEN_SOURCE_EXTENDED 1 +#define _DEFAULT_SOURCE 1 +#define _BSD_SOURCE 1 +#define _GNU_SOURCE 1 + +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <ctype.h> +#include <dlfcn.h> +#include <errno.h> +#include <fcntl.h> +#include <pthread.h> +#include <signal.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <netdb.h> +#include <netinet/in.h> +#include <netinet/ip.h> + +#include <microhttpd.h> +#include "kdc_locl.h" +#include "token_validator_plugin.h" +#include <getarg.h> +#include <roken.h> +#include <krb5.h> +#include <gssapi/gssapi.h> +#include <gssapi/gssapi_krb5.h> +#include <hx509.h> +#include "../lib/hx509/hx_locl.h" +#include <hx509-private.h> +#include <kadm5/admin.h> +#include <kadm5/private.h> +#include <kadm5/kadm5_err.h> + +#define heim_pcontext krb5_context +#define heim_pconfig krb5_context +#include <heimbase-svc.h> + +#if MHD_VERSION < 0x00097002 || defined(MHD_YES) +/* libmicrohttpd changed these from int valued macros to an enum in 0.9.71 */ +#ifdef MHD_YES +#undef MHD_YES +#undef MHD_NO +#endif +enum MHD_Result { MHD_NO = 0, MHD_YES = 1 }; +#define MHD_YES 1 +#define MHD_NO 0 +typedef int heim_mhd_result; +#else +typedef enum MHD_Result heim_mhd_result; +#endif + +#define BODYLEN_IS_STRLEN (~0) + +/* + * Libmicrohttpd is not the easiest API to use. It's got issues. + * + * One of the issues is how responses are handled, and the return value of the + * resource handler (MHD_NO -> close the connection, MHD_YES -> send response). + * Note that the handler could return MHD_YES without having set an HTTP + * response. + * + * There's memory management issues as well. + * + * Here we have to be careful about return values. + * + * Some of the functions defined here return just a krb5_error_code without + * having set an HTTP response on error. + * Others do set an HTTP response on error. + * The convention is to either set an HTTP response on error, or not at all, + * but not a mix of errors where for some the function will set a response and + * for others it won't. + * + * We do use some system error codes to stand in for errors here. + * Specifically: + * + * - EACCES -> authorization failed + * - EINVAL -> bad API usage + * - ENOSYS -> missing CSRF token but CSRF token required + * + * FIXME: We should rely only on krb5_set_error_message() and friends and make + * error responses only in route(), mapping krb5_error_code values to + * HTTP status codes. This would simplify the error handling convention + * here. + */ + +struct free_tend_list { + void *freeme1; + void *freeme2; + struct free_tend_list *next; +}; + +/* Our request description structure */ +typedef struct kadmin_request_desc { + HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS; + + struct MHD_Connection *connection; + krb5_times token_times; + /* + * FIXME + * + * Currently we re-use the authz framework from bx509d, using an + * `hx509_request' instance (an abstraction for CSRs) to represent the + * request because that is what the authz plugin uses that implements the + * policy we want checked here. + * + * This is inappropriate in the long-term in two ways: + * + * - the policy for certificates deals in SANs and EKUs, whereas the + * policy for ext_keytab deals in host-based service principal names, + * and there is not a one-to-one mapping of service names to EKUs; + * + * - using a type from libhx509 for representing requests for things that + * aren't certificates is really not appropriate no matter how similar + * the use cases for this all might be. + * + * What we need to do is develop a library that can represent requests for + * credentials via naming attributes like SANs and Kerberos principal + * names, but more arbitrary still than what `hx509_request' supports, and + * then invokes a plugin. + * + * Also, we might want to develop an in-tree authorization solution that is + * richer than what kadmin.acl supports now, storing grants in HDB entries + * and/or similar places. + * + * For expediency we use `hx509_request' here for now, impedance mismatches + * be damned. + */ + hx509_request req; /* For authz only */ + struct free_tend_list *free_list; + struct MHD_PostProcessor *pp; + 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; + char *cache_control; + char *csrf_token; + const char *method; + krb5_timestamp pw_end; + size_t post_data_size; + unsigned int response_set:1; + unsigned int materialize:1; + unsigned int rotate_now:1; + unsigned int rotate:1; + unsigned int revoke:1; + unsigned int create:1; + unsigned int ro:1; + unsigned int is_self:1; + char frombuf[128]; +} *kadmin_request_desc; + +static void +audit_trail(kadmin_request_desc r, krb5_error_code ret) +{ + const char *retname = NULL; + + /* + * Get a symbolic name for some error codes. + * + * Really, libcom_err should have a primitive for this, and ours could, but + * we can't use a system libcom_err if we extend ours. + */ +#define CASE(x) case x : retname = #x; break + switch (ret) { + case ENOSYS: retname = "ECSRFTOKENREQD"; break; + CASE(EINVAL); + CASE(ENOMEM); + CASE(EACCES); + CASE(HDB_ERR_NOT_FOUND_HERE); + CASE(HDB_ERR_WRONG_REALM); + CASE(HDB_ERR_EXISTS); + CASE(HDB_ERR_KVNO_NOT_FOUND); + CASE(HDB_ERR_NOENTRY); + CASE(HDB_ERR_NO_MKEY); + CASE(KRB5_KDC_UNREACH); + CASE(KADM5_FAILURE); + CASE(KADM5_AUTH_GET); + CASE(KADM5_AUTH_ADD); + CASE(KADM5_AUTH_MODIFY); + CASE(KADM5_AUTH_DELETE); + CASE(KADM5_AUTH_INSUFFICIENT); + CASE(KADM5_BAD_DB); + CASE(KADM5_DUP); + CASE(KADM5_RPC_ERROR); + CASE(KADM5_NO_SRV); + CASE(KADM5_BAD_HIST_KEY); + CASE(KADM5_NOT_INIT); + CASE(KADM5_UNK_PRINC); + CASE(KADM5_UNK_POLICY); + CASE(KADM5_BAD_MASK); + CASE(KADM5_BAD_CLASS); + CASE(KADM5_BAD_LENGTH); + CASE(KADM5_BAD_POLICY); + CASE(KADM5_BAD_PRINCIPAL); + CASE(KADM5_BAD_AUX_ATTR); + CASE(KADM5_BAD_HISTORY); + CASE(KADM5_BAD_MIN_PASS_LIFE); + CASE(KADM5_PASS_Q_TOOSHORT); + CASE(KADM5_PASS_Q_CLASS); + CASE(KADM5_PASS_Q_DICT); + CASE(KADM5_PASS_Q_GENERIC); + CASE(KADM5_PASS_REUSE); + CASE(KADM5_PASS_TOOSOON); + CASE(KADM5_POLICY_REF); + CASE(KADM5_INIT); + CASE(KADM5_BAD_PASSWORD); + CASE(KADM5_PROTECT_PRINCIPAL); + CASE(KADM5_BAD_SERVER_HANDLE); + CASE(KADM5_BAD_STRUCT_VERSION); + CASE(KADM5_OLD_STRUCT_VERSION); + CASE(KADM5_NEW_STRUCT_VERSION); + CASE(KADM5_BAD_API_VERSION); + CASE(KADM5_OLD_LIB_API_VERSION); + CASE(KADM5_OLD_SERVER_API_VERSION); + CASE(KADM5_NEW_LIB_API_VERSION); + CASE(KADM5_NEW_SERVER_API_VERSION); + CASE(KADM5_SECURE_PRINC_MISSING); + CASE(KADM5_NO_RENAME_SALT); + CASE(KADM5_BAD_CLIENT_PARAMS); + CASE(KADM5_BAD_SERVER_PARAMS); + CASE(KADM5_AUTH_LIST); + CASE(KADM5_AUTH_CHANGEPW); + CASE(KADM5_BAD_TL_TYPE); + CASE(KADM5_MISSING_CONF_PARAMS); + CASE(KADM5_BAD_SERVER_NAME); + CASE(KADM5_KS_TUPLE_NOSUPP); + CASE(KADM5_SETKEY3_ETYPE_MISMATCH); + CASE(KADM5_DECRYPT_USAGE_NOSUPP); + CASE(KADM5_POLICY_OP_NOSUPP); + CASE(KADM5_KEEPOLD_NOSUPP); + CASE(KADM5_AUTH_GET_KEYS); + CASE(KADM5_ALREADY_LOCKED); + CASE(KADM5_NOT_LOCKED); + CASE(KADM5_LOG_CORRUPT); + CASE(KADM5_LOG_NEEDS_UPGRADE); + CASE(KADM5_BAD_SERVER_HOOK); + CASE(KADM5_SERVER_HOOK_NOT_FOUND); + CASE(KADM5_OLD_SERVER_HOOK_VERSION); + CASE(KADM5_NEW_SERVER_HOOK_VERSION); + CASE(KADM5_READ_ONLY); + case 0: + retname = "SUCCESS"; + break; + default: + retname = NULL; + break; + } + heim_audit_trail((heim_svc_req_desc)r, ret, retname); +} + +static krb5_log_facility *logfac; +static pthread_key_t k5ctx; + +static krb5_error_code +get_krb5_context(krb5_context *contextp) +{ + krb5_error_code ret; + + if ((*contextp = pthread_getspecific(k5ctx))) + return 0; + + ret = krb5_init_context(contextp); + /* XXX krb5_set_log_dest(), warn_dest, debug_dest */ + if (ret == 0) + (void) pthread_setspecific(k5ctx, *contextp); + return ret; +} + +typedef enum { + CSRF_PROT_UNSPEC = 0, + CSRF_PROT_GET_WITH_HEADER = 1, + CSRF_PROT_GET_WITH_TOKEN = 2, + CSRF_PROT_POST_WITH_HEADER = 8, + CSRF_PROT_POST_WITH_TOKEN = 16, +} csrf_protection_type; + +static csrf_protection_type csrf_prot_type = CSRF_PROT_UNSPEC; +static int port = -1; +static int help_flag; +static int allow_GET_flag = -1; +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 getarg_strings csrf_prot_type_strs; +static const char *csrf_header = "X-CSRF"; +static const char *cert_file; +static const char *priv_key_file; +static const char *cache_dir; +static const char *realm; +static const char *hdb; +static const char *primary_server_URI; +static const char *kadmin_server; +static const char *writable_kadmin_server; +static const char *stash_file; +static const char *kadmin_client_name = "httpkadmind/admin"; +static const char *kadmin_client_keytab; +static struct getarg_strings auth_types; + +#define set_conf(c, f, v, b) \ + if (v) { \ + if (((c).f = strdup(v)) == NULL) \ + goto enomem; \ + conf.mask |= b; \ + } + +/* + * Does NOT set an HTTP response, naturally, as it doesn't even have access to + * the connection. + */ +static krb5_error_code +get_kadm_handle(krb5_context context, + const char *want_realm, + int want_write, + void **kadm_handle) +{ + kadm5_config_params conf; + krb5_error_code ret; + + /* + * If the caller wants to write and we are configured to redirect in that + * case, then trigger a redirect by returning KADM5_READ_ONLY. + */ + if (want_write && local_hdb_read_only && primary_server_URI) + return KADM5_READ_ONLY; + if (want_write && read_only) + return KADM5_READ_ONLY; + + /* + * Configure kadm5 connection. + * + * Note that all of these are optional, and will be found in krb5.conf or, + * in some cases, in DNS, as needed. + */ + memset(&conf, 0, sizeof(conf)); + conf.realm = NULL; + conf.dbname = NULL; + conf.stash_file = NULL; + conf.admin_server = NULL; + conf.readonly_admin_server = NULL; + set_conf(conf, realm, want_realm, KADM5_CONFIG_REALM); + set_conf(conf, dbname, hdb, KADM5_CONFIG_DBNAME); + set_conf(conf, stash_file, stash_file, KADM5_CONFIG_STASH_FILE); + + /* + * 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. + */ + if (writable_kadmin_server) + set_conf(conf, admin_server, writable_kadmin_server, KADM5_CONFIG_ADMIN_SERVER); + if (kadmin_server) + set_conf(conf, readonly_admin_server, kadmin_server, + KADM5_CONFIG_READONLY_ADMIN_SERVER); + ret = kadm5_c_init_with_skey_ctx(context, + kadmin_client_name, + kadmin_client_keytab, + KADM5_ADMIN_SERVICE, + &conf, + 0, /* struct_version */ + 0, /* api_version */ + kadm_handle); + goto out; + +enomem: + ret = krb5_enomem(context); + +out: + free(conf.readonly_admin_server); + free(conf.admin_server); + free(conf.stash_file); + free(conf.dbname); + free(conf.realm); + return ret; +} + +static krb5_error_code resp(kadmin_request_desc, int, krb5_error_code, + enum MHD_ResponseMemoryMode, const char *, + const void *, size_t, const char *); +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, krb5_error_code, const char *); +static krb5_error_code bad_405(kadmin_request_desc, const char *); +/*static krb5_error_code bad_500(kadmin_request_desc, krb5_error_code, const char *);*/ +static krb5_error_code bad_503(kadmin_request_desc, krb5_error_code, const char *); + +static int +validate_token(kadmin_request_desc r) +{ + krb5_error_code ret; + const char *token; + const char *host; + char token_type[64]; /* Plenty */ + char *p; + krb5_data tok; + size_t host_len, brk, i; + + memset(&r->token_times, 0, sizeof(r->token_times)); + host = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, + MHD_HTTP_HEADER_HOST); + if (host == NULL) + return bad_400(r, EINVAL, "Host header is missing"); + + /* Exclude port number here (IPv6-safe because of the below) */ + host_len = ((p = strchr(host, ':'))) ? p - host : strlen(host); + + token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, + MHD_HTTP_HEADER_AUTHORIZATION); + if (token == NULL) + return bad_401(r, "Authorization token is missing"); + brk = strcspn(token, " \t"); + if (token[brk] == '\0' || brk > sizeof(token_type) - 1) + return bad_401(r, "Authorization token is missing"); + memcpy(token_type, token, brk); + token_type[brk] = '\0'; + token += brk + 1; + tok.length = strlen(token); + tok.data = (void *)(uintptr_t)token; + + for (i = 0; i < audiences.num_strings; i++) + if (strncasecmp(host, audiences.strings[i], host_len) == 0 && + audiences.strings[i][host_len] == '\0') + break; + if (i == audiences.num_strings) + return bad_403(r, EINVAL, "Host: value is not accepted here"); + + r->sname = strdup(host); /* No need to check for ENOMEM here */ + + ret = kdc_validate_token(r->context, NULL /* realm */, token_type, &tok, + (const char **)&audiences.strings[i], 1, + &r->cprinc, &r->token_times); + if (ret) + return bad_403(r, ret, "Token validation failed"); + if (r->cprinc == NULL) + return bad_403(r, ret, + "Could not extract a principal name from token"); + ret = krb5_unparse_name(r->context, r->cprinc, &r->cname); + if (ret) + return bad_503(r, ret, + "Could not extract a principal name from token"); + return 0; +} + +static void +k5_free_context(void *ctx) +{ + krb5_free_context(ctx); +} + +#ifndef HAVE_UNLINKAT +static int +unlink1file(const char *dname, const char *name) +{ + char p[PATH_MAX]; + + if (strlcpy(p, dname, sizeof(p)) < sizeof(p) && + strlcat(p, "/", sizeof(p)) < sizeof(p) && + strlcat(p, name, sizeof(p)) < sizeof(p)) + return unlink(p); + return ERANGE; +} +#endif + +static void +rm_cache_dir(void) +{ + struct dirent *e; + DIR *d; + + /* + * This works, but not on Win32: + * + * (void) simple_execlp("rm", "rm", "-rf", cache_dir, NULL); + * + * We make no directories in `cache_dir', so we need not recurse. + */ + if ((d = opendir(cache_dir)) == NULL) + return; + + while ((e = readdir(d))) { +#ifdef HAVE_UNLINKAT + /* + * Because unlinkat() takes a directory FD, implementing one for + * libroken is tricky at best. Instead we might want to implement an + * rm_dash_rf() function in lib/roken. + */ + (void) unlinkat(dirfd(d), e->d_name, 0); +#else + (void) unlink1file(cache_dir, e->d_name); +#endif + } + (void) closedir(d); + (void) rmdir(cache_dir); +} + +/* + * Work around older libmicrohttpd not strduping response header values when + * set. + */ +static HEIMDAL_THREAD_LOCAL struct redirect_uri { + char uri[4096]; + size_t len; + size_t first_param; + int valid; +} redirect_uri; + +static void +redirect_uri_appends(struct redirect_uri *redirect, + const char *s) +{ + size_t sz, len; + char *p; + + if (!redirect->valid || redirect->len >= sizeof(redirect->uri) - 1) { + redirect->valid = 0; + return; + } + /* Optimize strlcpy by using redirect->uri + redirect->len */ + p = redirect->uri + redirect->len; + sz = sizeof(redirect->uri) - redirect->len; + if ((len = strlcpy(p, s, sz)) >= sz) + redirect->valid = 0; + else + redirect->len += len; +} + +static heim_mhd_result +make_redirect_uri_param_cb(void *d, + enum MHD_ValueKind kind, + const char *key, + const char *val) +{ + struct redirect_uri *redirect = d; + + redirect_uri_appends(redirect, redirect->first_param ? "?" : "&"); + redirect_uri_appends(redirect, key); + if (val) { + redirect_uri_appends(redirect, "="); + redirect_uri_appends(redirect, val); + } + redirect->first_param = 0; + return MHD_YES; +} + +static const char * +make_redirect_uri(kadmin_request_desc r, const char *base) +{ + redirect_uri.len = 0; + redirect_uri.uri[0] = '\0'; + redirect_uri.valid = 1; + redirect_uri.first_param = 1; + + redirect_uri_appends(&redirect_uri, base); /* Redirect to primary URI base */ + redirect_uri_appends(&redirect_uri, r->reqtype); /* URI local-part */ + (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND, + make_redirect_uri_param_cb, + &redirect_uri); + return redirect_uri.valid ? redirect_uri.uri : NULL; +} + + +/* + * XXX Shouldn't be a body, but a status message. The body should be + * configurable to be from a file. MHD doesn't give us a way to set the + * response status message though, just the body. + * + * Calls audit_trail(). + * + * Returns -1 if something terrible happened, which should ultimately cause + * route() to return MHD_NO, which should cause libmicrohttpd to close the + * connection to the user-agent. + * + * Returns 0 in all other cases. + */ +static krb5_error_code +resp(kadmin_request_desc r, + int http_status_code, + krb5_error_code ret, + enum MHD_ResponseMemoryMode rmmode, + const char *content_type, + const void *body, + size_t bodylen, + const char *token) +{ + struct MHD_Response *response; + int mret = MHD_YES; + + if (r->response_set) { + krb5_log_msg(r->context, logfac, 1, NULL, + "Internal error; attempted to set a second response"); + return 0; + } + + (void) gettimeofday(&r->tv_end, NULL); + audit_trail(r, ret); + + if (body && bodylen == BODYLEN_IS_STRLEN) + bodylen = strlen(body); + + response = MHD_create_response_from_buffer(bodylen, rk_UNCONST(body), + rmmode); + if (response == NULL) + return -1; + mret = MHD_add_response_header(response, MHD_HTTP_HEADER_AGE, "0"); + if (mret == MHD_YES && http_status_code == MHD_HTTP_OK) { + krb5_timestamp now; + + free(r->cache_control); + r->cache_control = NULL; + krb5_timeofday(r->context, &now); + if (r->pw_end && r->pw_end > now) { + if (asprintf(&r->cache_control, "no-store, max-age=%lld", + (long long)r->pw_end - now) == -1 || + r->cache_control == NULL) + /* Soft handling of ENOMEM here */ + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-store, max-age=3600"); + else + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_CACHE_CONTROL, + r->cache_control); + + } else + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-store, max-age=0"); + } else { + /* Shouldn't happen */ + mret = MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL, + "no-store, max-age=0"); + } + if (mret == MHD_YES && http_status_code == MHD_HTTP_UNAUTHORIZED) { + size_t i; + + if (auth_types.num_strings < 1) + http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE; + else + for (i = 0; mret == MHD_YES && i < auth_types.num_strings; i++) + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_WWW_AUTHENTICATE, + auth_types.strings[i]); + } else if (mret == MHD_YES && http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) { + const char *redir = make_redirect_uri(r, primary_server_URI); + + if (redir) + mret = MHD_add_response_header(response, MHD_HTTP_HEADER_LOCATION, + redir); + else + /* XXX Find a way to set a new response body; log */ + http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE; + } + + if (mret == MHD_YES && r->csrf_token) + mret = MHD_add_response_header(response, + "X-CSRF-Token", + r->csrf_token); + + if (mret == MHD_YES && content_type) { + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_CONTENT_TYPE, + content_type); + } + if (mret != MHD_NO) + mret = MHD_queue_response(r->connection, http_status_code, response); + MHD_destroy_response(response); + r->response_set = 1; + return mret == MHD_NO ? -1 : 0; +} + +static krb5_error_code +bad_reqv(kadmin_request_desc r, + krb5_error_code code, + int http_status_code, + const char *fmt, + va_list ap) +{ + krb5_error_code ret; + krb5_context context = NULL; + const char *k5msg = NULL; + const char *emsg = NULL; + char *formatted = NULL; + char *msg = NULL; + + context = r->context; + if (r->hcontext && r->kv) + heim_audit_setkv_number((heim_svc_req_desc)r, "http-status-code", + http_status_code); + (void) gettimeofday(&r->tv_end, NULL); + if (code == ENOMEM) { + if (context) + krb5_log_msg(context, logfac, 1, NULL, "Out of memory"); + return resp(r, http_status_code, code, MHD_RESPMEM_PERSISTENT, + NULL, fmt, BODYLEN_IS_STRLEN, NULL); + } + + 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->hcontext) + heim_audit_addreason((heim_svc_req_desc)r, "%s", formatted); + krb5_free_error_message(context, k5msg); + + if (ret == -1 || msg == NULL) { + if (context) + krb5_log_msg(context, logfac, 1, NULL, "Out of memory"); + return resp(r, MHD_HTTP_SERVICE_UNAVAILABLE, ENOMEM, + MHD_RESPMEM_PERSISTENT, NULL, + "Out of memory", BODYLEN_IS_STRLEN, NULL); + } + + ret = resp(r, http_status_code, code, MHD_RESPMEM_MUST_COPY, + NULL, msg, BODYLEN_IS_STRLEN, 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, krb5_error_code ret, const char *name) +{ + return bad_req(r, ret, 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_413(kadmin_request_desc r) +{ + return bad_req(r, E2BIG, MHD_HTTP_METHOD_NOT_ALLOWED, + "POST request body too large"); +} + +static krb5_error_code +bad_method_want_POST(kadmin_request_desc r) +{ + return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED, + "Use POST for making changes to principals"); +} + +#if 0 +static krb5_error_code +bad_500(kadmin_request_desc r, + krb5_error_code ret, + const char *reason) +{ + return bad_req(r, ret, MHD_HTTP_INTERNAL_SERVER_ERROR, + "Internal error: %s", reason); +} +#endif + +static krb5_error_code +bad_503(kadmin_request_desc r, + krb5_error_code ret, + const char *reason) +{ + return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Service unavailable: %s", reason); +} + +static krb5_error_code +good_ext_keytab(kadmin_request_desc r) +{ + krb5_error_code ret; + size_t bodylen; + void *body; + char *p; + + if (!r->keytab_name || !(p = strchr(r->keytab_name, ':'))) + return bad_503(r, EINVAL, "Internal error (no keytab produced)"); + p++; + if (strncmp(p, cache_dir, strlen(cache_dir)) != 0) + return bad_503(r, EINVAL, "Internal error"); + ret = rk_undumpdata(p, &body, &bodylen); + if (ret) + return bad_503(r, ret, "Could not recover keytab from temp file"); + + ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY, + "application/octet-stream", body, bodylen, NULL); + free(body); + return ret; +} + +static krb5_error_code +check_service_name(kadmin_request_desc r, const char *name) +{ + if (name == NULL || name[0] == '\0' || + strchr(name, '/') || strchr(name, '\\') || strchr(name, '@') || + strcmp(name, "krbtgt") == 0 || + strcmp(name, "iprop") == 0 || + strcmp(name, "kadmin") == 0 || + strcmp(name, "hprop") == 0 || + strcmp(name, "WELLKNOWN") == 0 || + strcmp(name, "K") == 0) { + krb5_set_error_message(r->context, EACCES, + "No one is allowed to fetch keys for " + "Heimdal service %s", name); + return EACCES; + } + if (strcmp(name, "root") != 0 && + strcmp(name, "host") != 0 && + strcmp(name, "exceed") != 0) + return 0; + if (krb5_config_get_bool_default(r->context, NULL, FALSE, + "ext_keytab", + "csr_authorizer_handles_svc_names", + NULL)) + return 0; + krb5_set_error_message(r->context, EACCES, + "No one is allowed to fetch keys for " + "service \"%s\" because of authorizer " + "limitations", name); + return EACCES; +} + +static heim_mhd_result +param_cb(void *d, + enum MHD_ValueKind kind, + const char *key, + const char *val) +{ + kadmin_request_desc r = d; + krb5_error_code ret = 0; + heim_string_t s = NULL; + + /* + * Multi-valued params: + * + * - spn=<service>/<hostname> + * - dNSName=<hostname> + * - service=<service> + * + * Single-valued params: + * + * - realm=<REALM> + * - materialize=true -- create a concrete princ where it's virtual + * - enctypes=... -- key-salt types + * - revoke=true -- delete old keys (concrete princs only) + * - rotate=true -- change keys (no-op for virtual princs) + * - create=true -- create a concrete princ + * - ro=true -- perform no writes + */ + + if (strcmp(key, "realm") == 0 && val) { + if (!r->realm && !(r->realm = strdup(val))) + ret = krb5_enomem(r->context); + } else if (strcmp(key, "materialize") == 0 || + strcmp(key, "revoke") == 0 || + strcmp(key, "rotate") == 0 || + strcmp(key, "create") == 0 || + strcmp(key, "ro") == 0) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_option", "%s", key); + if (!val || strcmp(val, "true") != 0) + krb5_set_error_message(r->context, ret = EINVAL, + "get-keys \"%s\" q-param accepts " + "only \"true\"", key); + else if (strcmp(key, "materialize") == 0) + r->materialize = 1; + else if (strcmp(key, "revoke") == 0) + r->revoke = 1; + else if (strcmp(key, "rotate") == 0) + r->rotate = 1; + else if (strcmp(key, "create") == 0) + r->create = 1; + else if (strcmp(key, "ro") == 0) + r->ro = 1; + } else if (strcmp(key, "dNSName") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_dNSName", "%s", val); + if (r->is_self) { + krb5_set_error_message(r->context, ret = EACCES, + "only one service may be requested for self"); + } else if (strchr(val, '.') == NULL) { + krb5_set_error_message(r->context, ret = EACCES, + "dNSName must have at least one '.' in it"); + } else { + s = heim_string_create(val); + if (!s) + ret = krb5_enomem(r->context); + else + ret = heim_array_append_value(r->hostnames, s); + } + if (ret == 0) + ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val); + } else if (strcmp(key, "service") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_service", "%s", val); + if (r->is_self) + krb5_set_error_message(r->context, ret = EACCES, + "use \"spn\" for self"); + else + ret = check_service_name(r, val); + if (ret == 0) { + s = heim_string_create(val); + if (!s) + ret = krb5_enomem(r->context); + else + ret = heim_array_append_value(r->service_names, s); + } + } else if (strcmp(key, "enctypes") == 0 && val) { + r->enctypes = strdup(val); + if (!(r->enctypes = strdup(val))) + ret = krb5_enomem(r->context); + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_enctypes", "%s", val); + } else if (r->is_self && strcmp(key, "spn") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_spn", "%s", val); + krb5_set_error_message(r->context, ret = EACCES, + "only one service may be requested for self"); + } else if (strcmp(key, "spn") == 0 && val) { + krb5_principal p = NULL; + const char *hostname = ""; + + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_spn", "%s", val); + + ret = krb5_parse_name_flags(r->context, val, + KRB5_PRINCIPAL_PARSE_NO_DEF_REALM, &p); + if (ret == 0 && krb5_principal_get_realm(r->context, p) == NULL) + ret = krb5_principal_set_realm(r->context, p, + r->realm ? r->realm : realm); + + /* + * The SPN has to have two components. + * + * TODO: Support more components? Support AD-style NetBIOS computer + * account names? + */ + if (ret == 0 && krb5_principal_get_num_comp(r->context, p) != 2) + ret = ENOTSUP; + + /* + * Allow only certain service names. Except that when + * the SPN == the requestor's principal name then allow the "host" + * service name. + */ + if (ret == 0) { + const char *service = + krb5_principal_get_comp_string(r->context, p, 0); + + if (strcmp(service, "host") == 0 && + krb5_principal_compare(r->context, p, r->cprinc) && + !r->is_self && + heim_array_get_length(r->hostnames) == 0 && + heim_array_get_length(r->spns) == 0) { + r->is_self = 1; + } else + ret = check_service_name(r, service); + } + if (ret == 0 && !krb5_principal_compare(r->context, p, r->cprinc)) + ret = check_service_name(r, + krb5_principal_get_comp_string(r->context, + p, 0)); + if (ret == 0) { + hostname = krb5_principal_get_comp_string(r->context, p, 1); + if (!hostname || !strchr(hostname, '.')) + krb5_set_error_message(r->context, ret = ENOTSUP, + "Only host-based service names supported"); + } + if (ret == 0 && r->realm) + ret = krb5_principal_set_realm(r->context, p, r->realm); + else if (ret == 0 && realm) + ret = krb5_principal_set_realm(r->context, p, realm); + if (ret == 0) + ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req, + hostname); + if (ret == 0 && !(s = heim_string_create(val))) + ret = krb5_enomem(r->context); + if (ret == 0) + ret = heim_array_append_value(r->spns, s); + krb5_free_principal(r->context, p); + +#if 0 + /* The authorizer probably doesn't know what to do with this */ + ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req, val); +#endif + } else { + /* Produce error for unknown params */ + heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_unknown", TRUE); + krb5_set_error_message(r->context, ret = ENOTSUP, + "Query parameter %s not supported", key); + } + if (ret && !r->error_code) + r->error_code = ret; + heim_release(s); + return ret ? MHD_NO /* Stop iterating */ : MHD_YES; +} + +static krb5_error_code +authorize_req(kadmin_request_desc r) +{ + krb5_error_code ret; + + r->is_self = 0; + ret = hx509_request_init(r->context->hx509ctx, &r->req); + if (ret) + return bad_enomem(r, ret); + (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND, + param_cb, r); + ret = r->error_code; + if (ret == EACCES) + return bad_403(r, ret, "Not authorized to requested principal(s)"); + if (ret) + return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not handle query parameters"); + if (r->is_self) + ret = 0; + else + ret = kdc_authorize_csr(r->context, "ext_keytab", r->req, r->cprinc); + if (ret == EACCES || ret == EINVAL || ret == ENOTSUP || + ret == KRB5KDC_ERR_POLICY) + return bad_403(r, ret, "Not authorized to requested principal(s)"); + if (ret) + return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Error checking authorization"); + return ret; +} + +static krb5_error_code +make_keytab(kadmin_request_desc r) +{ + krb5_error_code ret = 0; + int fd = -1; + + r->keytab_name = NULL; + if (asprintf(&r->keytab_name, "FILE:%s/kt-XXXXXX", cache_dir) == -1 || + r->keytab_name == NULL) + ret = krb5_enomem(r->context); + if (ret == 0) + fd = mkstemp(r->keytab_name + sizeof("FILE:") - 1); + if (ret == 0 && fd == -1) + ret = errno; + if (ret == 0) + ret = krb5_kt_resolve(r->context, r->keytab_name, &r->keytab); + if (fd != -1) + (void) close(fd); + return ret; +} + +static krb5_error_code +write_keytab(kadmin_request_desc r, + kadm5_principal_ent_rec *princ, + const char *unparsed) +{ + krb5_error_code ret = 0; + krb5_keytab_entry key; + size_t i; + + if (princ->n_key_data <= 0) + return 0; + + if (kadm5_some_keys_are_bogus(princ->n_key_data, &princ->key_data[0])) { + krb5_warn(r->context, ret, + "httpkadmind running with insufficient kadmin privilege " + "for extracting keys for %s", unparsed); + krb5_log_msg(r->context, logfac, 1, NULL, + "httpkadmind running with insufficient kadmin privilege " + "for extracting keys for %s", unparsed); + return EACCES; + } + + memset(&key, 0, sizeof(key)); + for (i = 0; ret == 0 && i < princ->n_key_data; i++) { + krb5_key_data *kd = &princ->key_data[i]; + + key.principal = princ->principal; + key.vno = kd->key_data_kvno; + key.keyblock.keytype = kd->key_data_type[0]; + key.keyblock.keyvalue.length = kd->key_data_length[0]; + key.keyblock.keyvalue.data = kd->key_data_contents[0]; + + /* + * FIXME kadm5 doesn't give us set_time here. If it gave us the + * KeyRotation metadata, we could compute it. But this might be a + * concrete principal with concrete keys, in which case we can't. + * + * To fix this we need to extend the protocol and the API. + */ + key.timestamp = time(NULL); + + ret = krb5_kt_add_entry(r->context, r->keytab, &key); + } + if (ret) + krb5_warn(r->context, ret, + "Failed to write keytab entries for %s", unparsed); + + return ret; +} + +static void +random_password(krb5_context context, char *buf, size_t buflen) +{ + static const char chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,"; + char p[32]; + size_t i; + char b; + + buflen--; + for (i = 0; i < buflen; i++) { + if (i % sizeof(p) == 0) + krb5_generate_random_block(p, sizeof(p)); + b = p[i % sizeof(p)]; + buf[i] = chars[b % (sizeof(chars) - 1)]; + } + buf[i] = '\0'; +} + +static krb5_error_code +make_kstuple(krb5_context context, + kadm5_principal_ent_rec *p, + krb5_key_salt_tuple **kstuple, + size_t *n_kstuple) +{ + size_t i; + + *kstuple = 0; + *n_kstuple = 0; + + if (p->n_key_data < 1) + return 0; + *kstuple = calloc(p->n_key_data, sizeof (**kstuple)); + for (i = 0; *kstuple && i < p->n_key_data; i++) { + if (p->key_data[i].key_data_kvno == p->kvno) { + (*kstuple)[i].ks_enctype = p->key_data[i].key_data_type[0]; + (*kstuple)[i].ks_salttype = p->key_data[i].key_data_type[1]; + (*n_kstuple)++; + } + } + return *kstuple ? 0 :krb5_enomem(context); +} + +/* Copied from kadmin/util.c */ +struct units kdb_attrs[] = { + { "auth-data-reqd", KRB5_KDB_AUTH_DATA_REQUIRED }, + { "no-auth-data-reqd", KRB5_KDB_NO_AUTH_DATA_REQUIRED }, + { "disallow-client", KRB5_KDB_DISALLOW_CLIENT }, + { "virtual", KRB5_KDB_VIRTUAL }, + { "virtual-keys", KRB5_KDB_VIRTUAL_KEYS }, + { "allow-digest", KRB5_KDB_ALLOW_DIGEST }, + { "allow-kerberos4", KRB5_KDB_ALLOW_KERBEROS4 }, + { "trusted-for-delegation", KRB5_KDB_TRUSTED_FOR_DELEGATION }, + { "ok-as-delegate", KRB5_KDB_OK_AS_DELEGATE }, + { "new-princ", KRB5_KDB_NEW_PRINC }, + { "support-desmd5", KRB5_KDB_SUPPORT_DESMD5 }, + { "pwchange-service", KRB5_KDB_PWCHANGE_SERVICE }, + { "disallow-svr", KRB5_KDB_DISALLOW_SVR }, + { "requires-pw-change", KRB5_KDB_REQUIRES_PWCHANGE }, + { "requires-hw-auth", KRB5_KDB_REQUIRES_HW_AUTH }, + { "requires-pre-auth", KRB5_KDB_REQUIRES_PRE_AUTH }, + { "disallow-all-tix", KRB5_KDB_DISALLOW_ALL_TIX }, + { "disallow-dup-skey", KRB5_KDB_DISALLOW_DUP_SKEY }, + { "disallow-proxiable", KRB5_KDB_DISALLOW_PROXIABLE }, + { "disallow-renewable", KRB5_KDB_DISALLOW_RENEWABLE }, + { "disallow-tgt-based", KRB5_KDB_DISALLOW_TGT_BASED }, + { "disallow-forwardable", KRB5_KDB_DISALLOW_FORWARDABLE }, + { "disallow-postdated", KRB5_KDB_DISALLOW_POSTDATED }, + { NULL, 0 } +}; + +/* + * Determine the default/allowed attributes for some new principal. + */ +static krb5_flags +create_attributes(kadmin_request_desc r, krb5_const_principal p) +{ + krb5_error_code ret; + const char *srealm = krb5_principal_get_realm(r->context, p); + const char *svc; + const char *hn; + + /* Has to be a host-based service principal (for now) */ + if (krb5_principal_get_num_comp(r->context, p) != 2) + return 0; + + hn = krb5_principal_get_comp_string(r->context, p, 1); + svc = krb5_principal_get_comp_string(r->context, p, 0); + + while (hn && strchr(hn, '.') != NULL) { + kadm5_principal_ent_rec nsprinc; + krb5_principal nsp; + uint64_t a = 0; + const char *as; + + /* Try finding a virtual host-based service principal namespace */ + memset(&nsprinc, 0, sizeof(nsprinc)); + ret = krb5_make_principal(r->context, &nsp, srealm, + KRB5_WELLKNOWN_NAME, HDB_WK_NAMESPACE, + svc, hn, NULL); + if (ret == 0) + ret = kadm5_get_principal(r->kadm_handle, nsp, &nsprinc, + KADM5_PRINCIPAL | KADM5_ATTRIBUTES); + krb5_free_principal(r->context, nsp); + if (ret == 0) { + /* Found one; use it even if disabled, but drop that attribute */ + a = nsprinc.attributes & ~KRB5_KDB_DISALLOW_ALL_TIX; + kadm5_free_principal_ent(r->kadm_handle, &nsprinc); + return a; + } + + /* Fallback on krb5.conf */ + as = krb5_config_get_string(r->context, NULL, "ext_keytab", + "new_hostbased_service_principal_attributes", + svc, hn, NULL); + if (as) { + a = parse_flags(as, kdb_attrs, 0); + if (a == (uint64_t)-1) { + krb5_warnx(r->context, "Invalid value for [ext_keytab] " + "new_hostbased_service_principal_attributes"); + return 0; + } + return a; + } + + hn = strchr(hn + 1, '.'); + } + + return 0; +} + +/* + * Get keys for one principal. + * + * Does NOT set an HTTP response. + */ +static krb5_error_code +get_keys1(kadmin_request_desc r, const char *pname) +{ + kadm5_principal_ent_rec princ; + krb5_key_salt_tuple *kstuple = NULL; + krb5_error_code ret = 0; + krb5_principal p = NULL; + uint32_t mask = + KADM5_PRINCIPAL | KADM5_KVNO | KADM5_MAX_LIFE | KADM5_MAX_RLIFE | + KADM5_PW_EXPIRATION | 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]; + + memset(&princ, 0, sizeof(princ)); + princ.attributes = create_attributes(r, p); + + 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); + } + /* + * 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)) { + + 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; + } /* 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 (ret == 0) { + /* + * We will use the principal's password expiration to work out the + * value for the max-age Cache-Control. + * + * Virtual service principals will have their `pw_expiration' set to a + * time when the client should refetch keys. + * + * Concrete service principals will generally not have a non-zero + * `pw_expiration', but if we have a new_service_key_delay, then we'll + * use half of it as the max-age Cache-Control. + */ + if (princ.pw_expiration == 0) { + krb5_timestamp nskd = + krb5_config_get_time_default(r->context, NULL, 0, "hdb", + "new_service_key_delay", NULL); + if (nskd) + princ.pw_expiration = time(NULL) + (nskd >> 1); + } + + /* + * This service can be used to fetch more than one principal's keys, so + * the max-age Cache-Control should be derived from the soonest- + * "expiring" principal. + */ + if (r->pw_end == 0 || + (princ.pw_expiration < r->pw_end && princ.pw_expiration > time(NULL))) + r->pw_end = princ.pw_expiration; + } + if (freeit) + kadm5_free_principal_ent(r->kadm_handle, &princ); + krb5_free_principal(r->context, p); + return ret; +} + +static krb5_error_code check_csrf(kadmin_request_desc); + +/* + * Calls get_keys1() to extract each requested principal's keys. + * + * When this returns a response will have been set. + */ +static krb5_error_code +get_keysN(kadmin_request_desc r) +{ + krb5_error_code ret; + size_t nhosts; + size_t nsvcs; + size_t nspns; + size_t i, k; + + /* Parses and validates the request, then checks authorization */ + ret = authorize_req(r); + if (ret) + return ret; /* authorize_req() calls bad_req() on error */ + + /* + * If we have a r->kadm_handle already it's because we validated a CSRF + * token. It may not be a handle to a realm we wanted though. + */ + if (r->kadm_handle) + kadm5_destroy(r->kadm_handle); + r->kadm_handle = NULL; + ret = get_kadm_handle(r->context, r->realm ? r->realm : realm, + 0 /* want_write */, &r->kadm_handle); + if (ret) + return bad_404(r, ret, "Could not connect to realm"); + + nhosts = heim_array_get_length(r->hostnames); + nsvcs = heim_array_get_length(r->service_names); + nspns = heim_array_get_length(r->spns); + if (!nhosts && !nspns) + return bad_403(r, EINVAL, "No service principals requested"); + + if (nhosts && !nsvcs) { + heim_string_t s; + + if ((s = heim_string_create("HTTP")) == NULL) + ret = krb5_enomem(r->context); + if (ret == 0) + ret = heim_array_append_value(r->service_names, s); + heim_release(s); + nsvcs = 1; + if (ret) + return bad_503(r, ret, "Out of memory"); + } + + if (nspns + nsvcs * nhosts > + krb5_config_get_int_default(r->context, NULL, 400, + "ext_keytab", "get_keys_max_spns", NULL)) + return bad_403(r, EINVAL, "Requested keys for too many principals"); + + ret = make_keytab(r); + for (i = 0; ret == 0 && i < nsvcs; i++) { + const char *svc = + heim_string_get_utf8( + heim_array_get_value(r->service_names, i)); + + for (k = 0; ret == 0 && k < nhosts; k++) { + krb5_principal p = NULL; + const char *hostname = + heim_string_get_utf8( + heim_array_get_value(r->hostnames, k)); + char *spn = NULL; + + ret = krb5_make_principal(r->context, &p, + r->realm ? r->realm : realm, + svc, hostname, NULL); + if (ret == 0) + ret = krb5_unparse_name(r->context, p, &spn); + if (ret == 0) + ret = get_keys1(r, spn); + krb5_free_principal(r->context, p); + free(spn); + } + } + for (i = 0; ret == 0 && i < nspns; i++) { + ret = get_keys1(r, + heim_string_get_utf8(heim_array_get_value(r->spns, + i))); + } + switch (ret) { + case -1: + /* Can't happen */ + krb5_log_msg(r->context, logfac, 1, NULL, + "Failed to extract keys for unknown reasons"); + if (r->response_set) + return MHD_YES; + return bad_503(r, ret, "Could not get keys"); + case ENOSYS: + /* Our convention */ + return bad_method_want_POST(r); + case KADM5_READ_ONLY: + if (primary_server_URI) { + krb5_log_msg(r->context, logfac, 1, NULL, + "Redirect %s to primary server", r->cname); + return resp(r, MHD_HTTP_TEMPORARY_REDIRECT, KADM5_READ_ONLY, + MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL); + } else { + krb5_log_msg(r->context, logfac, 1, NULL, "HDB is read-only"); + return bad_403(r, ret, "HDB is read-only"); + } + case 0: + krb5_log_msg(r->context, logfac, 1, NULL, "Sent keytab to %s", + r->cname); + return good_ext_keytab(r); + default: + return bad_503(r, ret, "Could not get keys"); + } +} + +/* Copied from kdc/connect.c */ +static void +addr_to_string(krb5_context context, + struct sockaddr *addr, + char *str, + size_t len) +{ + krb5_error_code ret; + krb5_address a; + + ret = krb5_sockaddr2address(context, addr, &a); + if (ret == 0) { + ret = krb5_print_address(&a, str, len, &len); + krb5_free_address(context, &a); + } + if (ret) + snprintf(str, len, "<family=%d>", addr->sa_family); +} + +static void clean_req_desc(kadmin_request_desc); + +static krb5_error_code +set_req_desc(struct MHD_Connection *connection, + const char *method, + const char *url, + kadmin_request_desc *rp) +{ + const union MHD_ConnectionInfo *ci; + kadmin_request_desc r; + const char *token; + krb5_error_code ret; + + *rp = NULL; + if ((r = calloc(1, sizeof(*r))) == NULL) + return ENOMEM; + + (void) gettimeofday(&r->tv_start, NULL); + if ((ret = get_krb5_context(&r->context))) { + free(r); + return ret; + } + /* HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS fields */ + r->request.data = "<HTTP-REQUEST>"; + r->request.length = sizeof("<HTTP-REQUEST>"); + r->from = r->frombuf; + r->free_list = NULL; + r->config = NULL; + r->logf = logfac; + r->reqtype = url; + r->reason = NULL; + r->reply = NULL; + r->sname = NULL; + r->cname = NULL; + r->addr = NULL; + r->kv = heim_dict_create(10); + r->pp = NULL; + r->attributes = heim_dict_create(1); + /* Our fields */ + r->connection = connection; + r->kadm_handle = NULL; + r->hcontext = r->context->hcontext; + r->service_names = heim_array_create(); + r->hostnames = heim_array_create(); + r->spns = heim_array_create(); + r->keytab_name = NULL; + r->enctypes = NULL; + r->cache_control = NULL; + r->freeme1 = NULL; + r->method = method; + r->cprinc = NULL; + r->req = NULL; + r->sp = NULL; + ci = MHD_get_connection_info(connection, + MHD_CONNECTION_INFO_CLIENT_ADDRESS); + if (ci) { + r->addr = ci->client_addr; + addr_to_string(r->context, r->addr, r->frombuf, sizeof(r->frombuf)); + } + + if (r->kv) { + heim_audit_addkv((heim_svc_req_desc)r, 0, "method", "GET"); + heim_audit_addkv((heim_svc_req_desc)r, 0, "endpoint", "%s", r->reqtype); + } + token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, + MHD_HTTP_HEADER_AUTHORIZATION); + if (token && r->kv) { + const char *token_end; + + if ((token_end = strchr(token, ' ')) == NULL || + (token_end - token) > INT_MAX || (token_end - token) < 2) + heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "<unknown>"); + else + heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "%.*s", + (int)(token_end - token), token); + + } + + if (ret == 0 && r->kv == NULL) { + krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory"); + ret = r->error_code = ENOMEM; + } + if (ret == 0) + *rp = r; + else + clean_req_desc(r); + 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); + if (r->pp) + MHD_destroy_post_processor(r->pp); + hx509_request_free(&r->req); + heim_release(r->service_names); + heim_release(r->attributes); + 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->cache_control); + free(r->keytab_name); + free(r->csrf_token); + free(r->enctypes); + free(r->freeme1); + free(r->cname); + free(r->sname); + free(r->realm); + free(r); +} + +static void +cleanup_req(void *cls, + struct MHD_Connection *connection, + void **con_cls, + enum MHD_RequestTerminationCode toe) +{ + kadmin_request_desc r = *con_cls; + + (void)cls; + (void)connection; + (void)toe; + clean_req_desc(r); + *con_cls = NULL; +} + +/* Implements GETs of /get-keys */ +static krb5_error_code +get_keys(kadmin_request_desc r) +{ + if (r->cname == NULL || r->cprinc == NULL) + return bad_401(r, "Could not extract principal name from token"); + return get_keysN(r); /* Sets an HTTP response */ +} + +/* Implements GETs of /get-config */ +static krb5_error_code +get_config(kadmin_request_desc r) +{ + + kadm5_principal_ent_rec princ; + krb5_error_code ret; + krb5_principal p = NULL; + uint32_t mask = KADM5_PRINCIPAL | KADM5_TL_DATA; + krb5_tl_data *tl_next; + const char *pname; + /* Default configuration for principals that have none set: */ + size_t bodylen = sizeof("include /etc/krb5.conf\n") - 1; + void *body = "include /etc/krb5.conf\n"; + int freeit = 0; + + if (r->cname == NULL || r->cprinc == NULL) + return bad_401(r, "Could not extract principal name from token"); + /* + * No authorization needed -- configs are public. Though we do require + * authentication (above). + */ + + ret = get_kadm_handle(r->context, r->realm ? r->realm : realm, + 0 /* want_write */, &r->kadm_handle); + if (ret) + return bad_503(r, ret, "Could not access KDC database"); + + memset(&princ, 0, sizeof(princ)); + princ.key_data = NULL; + princ.tl_data = NULL; + + pname = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND, + "princ"); + if (pname == NULL) + pname = r->cname; + ret = krb5_parse_name(r->context, pname, &p); + if (ret == 0) { + ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask); + if (ret == 0) { + freeit = 1; + for (tl_next = princ.tl_data; tl_next; tl_next = tl_next->tl_data_next) { + if (tl_next->tl_data_type != KRB5_TL_KRB5_CONFIG) + continue; + bodylen = tl_next->tl_data_length; + body = tl_next->tl_data_contents; + break; + } + } else { + r->error_code = ret; + return bad_404(r, ret, "/get-config"); + } + } + + if (ret == 0) { + krb5_log_msg(r->context, logfac, 1, NULL, + "Returned krb5.conf contents to %s", r->cname); + ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY, + "application/text", body, bodylen, NULL); + } 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 ? r->realm : realm, + 0 /* want_write */, + &r->kadm_handle); + if (ret == 0) + ret = krb5_make_principal(r->context, &p, + r->realm ? r->realm : realm, + "WELLKNOWN", "CSRFTOKEN", NULL); + if (ret == 0) + ret = kadm5_get_principal(r->kadm_handle, p, &princ, + KADM5_PRINCIPAL | KADM5_KVNO | + KADM5_KEY_DATA); + if (ret == 0) + freeit = 1; + if (ret == 0 && princ.n_key_data < 1) + ret = KADM5_UNK_PRINC; + if (ret == 0) + for (i = 0; i < princ.n_key_data; i++) + if (princ.key_data[i].key_data_kvno == princ.kvno) + break; + if (ret == 0 && i == princ.n_key_data) + i = 0; /* Weird, but can't happen */ + + if (ret == 0 && (ctx = HMAC_CTX_new()) == NULL) + ret = krb5_enomem(r->context); + /* HMAC the token body and the client principal name */ + if (ret == 0) { + if (HMAC_Init_ex(ctx, princ.key_data[i].key_data_contents[0], + princ.key_data[i].key_data_length[0], EVP_sha256(), + NULL) == 0) { + HMAC_CTX_cleanup(ctx); + ret = krb5_enomem(r->context); + } else { + HMAC_Update(ctx, data.data, data.length); + HMAC_Update(ctx, r->cname, strlen(r->cname)); + HMAC_Final(ctx, mac, &maclen); + HMAC_CTX_cleanup(ctx); + krb5_data_free(&data); + data.length = maclen; + data.data = mac; + if (krb5_storage_write(sp, mac, maclen) != maclen) + ret = krb5_enomem(r->context); + } + } + krb5_free_principal(r->context, p); + if (freeit) + kadm5_free_principal_ent(r->kadm_handle, &princ); + if (ctx) + HMAC_CTX_free(ctx); + return ret; +} + +static krb5_error_code +make_csrf_token(kadmin_request_desc r, + const char *given, + char **token, + int64_t *age) +{ + 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 && + rk_base64_encode(data.data, data.length, token) < 0) + ret = errno; + krb5_storage_free(sp); + krb5_data_free(&data); + return ret; +} + +/* + * Returns system or krb5_error_code on error, but also calls resp() or bad_*() + * on error. + */ +static krb5_error_code +check_csrf(kadmin_request_desc r) +{ + krb5_error_code ret; + const char *given; + int64_t age; + size_t givenlen, expectedlen; + + if ((((csrf_prot_type & CSRF_PROT_GET_WITH_HEADER) && + strcmp(r->method, "GET") == 0) || + ((csrf_prot_type & CSRF_PROT_POST_WITH_HEADER) && + strcmp(r->method, "POST") == 0)) && + MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, + csrf_header) == NULL) { + ret = bad_req(r, EACCES, MHD_HTTP_FORBIDDEN, + "Request must have header \"%s\"", csrf_header); + return ret == -1 ? MHD_NO : MHD_YES; + } + + if (strcmp(r->method, "GET") == 0 && + !(csrf_prot_type & CSRF_PROT_GET_WITH_TOKEN)) + return 0; + if (strcmp(r->method, "POST") == 0 && + !(csrf_prot_type & CSRF_PROT_POST_WITH_TOKEN)) + return 0; + + given = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, + "X-CSRF-Token"); + ret = make_csrf_token(r, given, &r->csrf_token, &age); + if (ret) + return bad_503(r, ret, "Could not create a CSRF token"); + /* + * If CSRF token needed but missing, call resp() directly, bypassing + * bad_403(), to return a 403 with an expected CSRF token in the response. + */ + if (given == NULL) { + (void) resp(r, MHD_HTTP_FORBIDDEN, ENOSYS, MHD_RESPMEM_PERSISTENT, + NULL, "CSRF token needed; copy the X-CSRF-Token: response " + "header to your next POST", BODYLEN_IS_STRLEN, NULL); + return ENOSYS; + } + + /* Validate the CSRF token for this request */ + givenlen = strlen(given); + expectedlen = strlen(r->csrf_token); + if (givenlen != expectedlen || ct_memcmp(given, r->csrf_token, givenlen)) { + (void) bad_403(r, EACCES, "Invalid CSRF token"); + return EACCES; + } + if (age > 300) { /* XXX */ + (void) bad_403(r, EACCES, "CSRF token too old"); + return EACCES; + } + return 0; +} + +static krb5_error_code +health(const char *method, kadmin_request_desc r) +{ + if (strcmp(method, "HEAD") == 0) { + return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL, "", 0, + NULL); + } + return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL, + "To determine the health of the service, use the /get-config " + "end-point.\n", BODYLEN_IS_STRLEN, NULL); + +} + +static heim_mhd_result +ip(void *cls, + enum MHD_ValueKind kind, + const char *key, + const char *content_name, + const char *content_type, + const char *transfer_encoding, + const char *val, + uint64_t off, + size_t size) +{ + kadmin_request_desc r = cls; + struct free_tend_list *ftl = calloc(1, sizeof(*ftl)); + char *keydup = strdup(key); + char *valdup = strndup(val, size); + + (void)content_name; /* MIME attachment name */ + (void)content_type; + (void)transfer_encoding; + (void)off; /* Offset in POST data */ + + /* We're going to MHD_set_connection_value(), but we need copies */ + if (ftl == NULL || keydup == NULL || valdup == NULL) { + free(ftl); + free(keydup); + free(valdup); + return MHD_NO; + } + ftl->freeme1 = keydup; + ftl->freeme2 = valdup; + ftl->next = r->free_list; + r->free_list = ftl; + + return MHD_set_connection_value(r->connection, MHD_GET_ARGUMENT_KIND, + keydup, valdup); +} + +typedef krb5_error_code (*handler)(struct kadmin_request_desc *); + +struct route { + const char *local_part; + handler h; +} routes[] = { + { "/get-keys", get_keys }, + { "/get-config", get_config }, +}; + +static heim_mhd_result +route(void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **ctx) +{ + struct kadmin_request_desc *r = *ctx; + size_t i; + int ret; + + if (r == 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 want to make sure to send any 401s early, so we check + * WWW-Authenticate now, not later. + * + * We'll get called again to really do the processing. If we're + * handling a POST then we'll also get called with upload_data != NULL, + * possibly multiple times. + */ + if ((ret = set_req_desc(connection, method, url, &r))) + return MHD_NO; + *ctx = r; + + /* + * All requests other than /health require authentication and CSRF + * protection. + */ + if (strcmp(url, "/health") == 0) + return MHD_YES; + + /* Authenticate and do CSRF protection */ + ret = validate_token(r); + if (ret == 0) + ret = check_csrf(r); + + /* + * As this is the initial call to this handler, we must return now. + * + * If authentication or CSRF protection failed then we'll already have + * enqueued a 401, 403, or 5xx response and then we're done. + * + * If both authentication and CSRF protection succeeded then no + * response has been queued up and we'll get called again to finally + * process the request, then this entire if block will not be executed. + */ + return ret == -1 ? MHD_NO : MHD_YES; + } + + /* Validate HTTP method */ + if (strcmp(method, "GET") != 0 && + strcmp(method, "POST") != 0 && + strcmp(method, "HEAD") != 0) { + return bad_405(r, method) == -1 ? MHD_NO : MHD_YES; + } + + if ((strcmp(method, "HEAD") == 0 || strcmp(method, "GET") == 0) && + (strcmp(url, "/health") == 0 || strcmp(url, "/") == 0)) { + /* /health end-point -- no authentication, no CSRF, no nothing */ + return health(method, r) == -1 ? MHD_NO : MHD_YES; + } + + if (strcmp(method, "POST") == 0 && *upload_data_size != 0) { + /* + * Consume all the POST body and set form data as MHD_GET_ARGUMENT_KIND + * (as if they had been URI query parameters). + * + * We have to do this before we can MHD_queue_response() as MHD will + * not consume the rest of the request body on its own, so it's an + * error to MHD_queue_response() before we've done this, and if we do + * then MHD just closes the connection. + * + * 4KB should be more than enough buffer space for all the keys we + * expect. + */ + if (r->pp == NULL) + r->pp = MHD_create_post_processor(connection, 4096, ip, r); + if (r->pp == NULL) { + ret = bad_503(r, errno ? errno : ENOMEM, + "Could not consume POST data"); + return ret == -1 ? MHD_NO : MHD_YES; + } + if (r->post_data_size + *upload_data_size > 1UL<<17) { + return bad_413(r) == -1 ? MHD_NO : MHD_YES; + } + r->post_data_size += *upload_data_size; + if (MHD_post_process(r->pp, upload_data, + *upload_data_size) == MHD_NO) { + ret = bad_503(r, errno ? errno : ENOMEM, + "Could not consume POST data"); + return ret == -1 ? MHD_NO : MHD_YES; + } + *upload_data_size = 0; + return MHD_YES; + } + + /* + * Either this is a HEAD, a GET, or a POST whose request body has now been + * received completely and processed. + */ + + /* Allow GET? */ + if (strcmp(method, "GET") == 0 && !allow_GET_flag) { + /* No */ + return bad_405(r, method) == -1 ? MHD_NO : MHD_YES; + } + + for (i = 0; i < sizeof(routes)/sizeof(routes[0]); i++) { + if (strcmp(url, routes[i].local_part) != 0) + continue; + if (MHD_lookup_connection_value(r->connection, + MHD_HEADER_KIND, + "Referer") != NULL) { + ret = bad_req(r, EACCES, MHD_HTTP_FORBIDDEN, + "GET from browser not allowed"); + return ret == -1 ? MHD_NO : MHD_YES; + } + if (strcmp(method, "HEAD") == 0) + ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL, "", 0, + NULL); + else + ret = routes[i].h(r); + return ret == -1 ? MHD_NO : MHD_YES; + } + + ret = bad_404(r, ENOENT, url); + 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" }, + { "allow-GET", 0, arg_negative_flag, + &allow_GET_flag, NULL, NULL }, + { "csrf-header", 0, arg_string, &csrf_header, + "required request header", "HEADER-NAME" }, + { "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 +} + +static void +get_csrf_prot_type(krb5_context context) +{ + char * const *strs = csrf_prot_type_strs.strings; + size_t n = csrf_prot_type_strs.num_strings; + size_t i; + char **freeme = NULL; + + if (csrf_header == NULL) + csrf_header = krb5_config_get_string(context, NULL, "bx509d", + "csrf_protection_csrf_header", + NULL); + + if (n == 0) { + char * const *p; + + strs = freeme = krb5_config_get_strings(context, NULL, "bx509d", + "csrf_protection_type", NULL); + for (p = strs; p && p; p++) + n++; + } + + for (i = 0; i < n; i++) { + if (strcmp(strs[i], "GET-with-header") == 0) + csrf_prot_type |= CSRF_PROT_GET_WITH_HEADER; + else if (strcmp(strs[i], "GET-with-token") == 0) + csrf_prot_type |= CSRF_PROT_GET_WITH_TOKEN; + else if (strcmp(strs[i], "POST-with-header") == 0) + csrf_prot_type |= CSRF_PROT_POST_WITH_HEADER; + else if (strcmp(strs[i], "POST-with-token") == 0) + csrf_prot_type |= CSRF_PROT_POST_WITH_TOKEN; + } + free(freeme); + + /* + * For GETs we default to no CSRF protection as our GETable resources are + * safe and idempotent and we count on the browser not to make the + * responses available to cross-site requests. + * + * But, really, we don't want browsers even making these requests since, if + * the browsers behave correctly, then there's no point, and if they don't + * behave correctly then that could be catastrophic. Of course, there's no + * guarantee that a browser won't have other catastrophic bugs, but still, + * we should probably change this default in the future: + * + * if (!(csrf_prot_type & CSRF_PROT_GET_WITH_HEADER) && + * !(csrf_prot_type & CSRF_PROT_GET_WITH_TOKEN)) + * csrf_prot_type |= <whatever-the-new-default-should-be>; + */ + + /* + * For POSTs we default to CSRF protection with anti-CSRF tokens even + * though out POSTable resources are safe and idempotent when POSTed and we + * could count on the browser not to make the responses available to + * cross-site requests. + */ + if (!(csrf_prot_type & CSRF_PROT_POST_WITH_HEADER) && + !(csrf_prot_type & CSRF_PROT_POST_WITH_TOKEN)) + csrf_prot_type |= CSRF_PROT_POST_WITH_TOKEN; +} + +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 (writable_kadmin_server == NULL && kadmin_server == NULL && + !local_hdb && !local_hdb_read_only) + errx(1, "One of --local or --local-read-only must be given, or a " + "remote kadmind must be given"); + + if (audiences.num_strings == 0) { + char localhost[MAXHOSTNAMELEN]; + + ret = gethostname(localhost, sizeof(localhost)); + if (ret == -1) + errx(1, "Could not determine local hostname; use --audience"); + + if ((audiences.strings = + calloc(1, sizeof(audiences.strings[0]))) == NULL || + (audiences.strings[0] = strdup(localhost)) == NULL) + err(1, "Out of memory"); + audiences.num_strings = 1; + } + + if (daemonize && daemon_child_fd == -1) + daemon_child_fd = roken_detach_prep(argc, argv, "--daemon-child"); + daemonize = 0; + + argc -= optidx; + argv += optidx; + if (argc != 0) + usage(1); + + if ((errno = pthread_key_create(&k5ctx, k5_free_context))) + err(1, "Could not create thread-specific storage"); + + if ((errno = get_krb5_context(&context))) + err(1, "Could not init krb5 context (config file issue?)"); + + get_csrf_prot_type(context); + + 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); + kadm_handle = NULL; + + 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, + /* + * This is a connection access callback. We + * don't use it. + */ + NULL, NULL, + /* This is our request handler */ + route, (char *)NULL, + MHD_OPTION_SOCK_ADDR, &sin, + MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200, + MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10, + /* This is our request cleanup handler */ + MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL, + 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_NOTIFY_COMPLETED, cleanup_req, NULL, + 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_NOTIFY_COMPLETED, cleanup_req, NULL, + 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/third_party/heimdal/kdc/ipc_csr_authorizer.c b/third_party/heimdal/kdc/ipc_csr_authorizer.c new file mode 100644 index 0000000..f2ce03c --- /dev/null +++ b/third_party/heimdal/kdc/ipc_csr_authorizer.c @@ -0,0 +1,681 @@ +/* + * Copyright (c) 2019 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. + */ + +/* + * This plugin authorizes requested certificate SANs and EKUs by calling a + * service over IPC (Unix domain sockets on Linux/BSD/Illumos). + * + * The IPC protocol is request/response, with requests and responses sent as + * + * <length><string> + * + * where the <length> is 4 bytes, unsigned binary in network byte order, and + * <string> is an array of <length> bytes and does NOT include a NUL + * terminator. + * + * Requests are of the form: + * + * check <princ> <exttype>=<extvalue> ... + * + * where <princ> is a URL-escaped principal name, <exttype> is one of: + * + * - san_pkinit + * - san_xmpp + * - san_email + * - san_ms_upn + * - san_dnsname + * - eku + * + * and <extvalue> is a URL-escaped string representation of the SAN or OID. + * + * OIDs are in the form 1.2.3.4.5.6. + * + * Only characters other than alphanumeric, '@', '.', '-', '_', and '/' are + * URL-encoded. + * + * Responses are any of: + * + * - granted + * - denied + * - error message + * + * Example: + * + * C->S: check jane@TEST.H5L.SE san_dnsname=jane.foo.test.h5l.se eku=1.3.6.1.5.5.7.3.1 + * S->C: granted + * + * Only digitalSignature and nonRepudiation key usages are allowed. Requested + * key usages are not sent to the CSR authorizer IPC server. + */ + +#define _GNU_SOURCE 1 + +#include <sys/types.h> +#include <sys/stat.h> +#include <ctype.h> +#include <errno.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include <roken.h> +#include <heim-ipc.h> +#include <krb5.h> +#include <hx509.h> +#include <kdc.h> +#include <common_plugin.h> +#include <csr_authorizer_plugin.h> + +/* + * string_encode_sz() and string_encode() encode principal names and such to be + * safe for use in our IPC text messages. They function very much like URL + * encoders, but '~' also gets encoded, and '.' and '@' do not. + * + * An unescaper is not needed here. + */ +static size_t +string_encode_sz(const char *in) +{ + size_t sz = strlen(in); + + while (*in) { + char c = *(in++); + + switch (c) { + case '@': + case '.': + case '-': + case '_': + case '/': + continue; + default: + if (isalnum((unsigned char)c)) + continue; + sz += 2; + } + } + return sz; +} + +static char * +string_encode(const char *in) +{ + size_t len = strlen(in); + size_t sz = string_encode_sz(in); + size_t i, k; + char *s; + + if ((s = malloc(sz + 1)) == NULL) + return NULL; + s[sz] = '\0'; + + for (i = k = 0; i < len; i++) { + unsigned char c = ((const unsigned char *)in)[i]; + + switch (c) { + case '@': + case '.': + case '-': + case '_': + case '/': + s[k++] = c; + break; + default: + if (isalnum(c)) { + s[k++] = c; + } else { + s[k++] = '%'; + s[k++] = "0123456789abcdef"[(c&0xff)>>4]; + s[k++] = "0123456789abcdef"[(c&0x0f)]; + } + } + } + return s; +} + +static int +cmd_append(struct rk_strpool **cmd, const char *s0, ...) +{ + va_list ap; + const char *arg; + int ret = 0; + + if ((*cmd = rk_strpoolprintf(*cmd, "%s", s0)) == NULL) + return ENOMEM; + + va_start(ap, s0); + while ((arg = va_arg(ap, const char *))) { + char *s; + + if ((s = string_encode(arg)) == NULL) { + rk_strpoolfree(*cmd); + *cmd = NULL; + ret = ENOMEM; + goto out; + } + *cmd = rk_strpoolprintf(*cmd, "%s", s); + free(s); + if (*cmd == NULL) { + ret = ENOMEM; + goto out; + } + } + + out: + va_end(ap); + return ret; +} + +/* Like strpbrk(), but from the end of the string */ +static char * +strrpbrk(char *s, const char *accept) +{ + char *last = NULL; + char *p = s; + + do { + p = strpbrk(p, accept); + if (p != NULL) { + last = p; + p++; + } + } while (p != NULL); + return last; +} + +/* + * For /get-tgts we need to support partial authorization of requests. The + * hx509_request APIs support that. + * + * Here we just step through the IPC server's response and mark the + * corresponding request elements authorized so that /get-tgts can issue or not + * issue TGTs according to which requested principals are authorized and which + * are not. + */ +static int +mark_piecemeal_authorized(krb5_context context, + hx509_request csr, + heim_octet_string *rep) +{ + size_t san_idx = 0; + size_t eku_idx = 0; + char *s, *p, *rep2, *tok, *next = NULL; + int slow_path = 0; + int partial = 0; + int ret = 0; + + /* We have a data, but we want a C string */ + if ((rep2 = strndup(rep->data, rep->length)) == NULL) + return krb5_enomem(context); + + /* The first token should be "denied"; skip it */ + if ((s = strchr(rep2, ' ')) == NULL) { + free(rep2); + return EACCES; + } + s++; + + while ((tok = strtok_r(s, ",", &next))) { + hx509_san_type san_type, san_type2; + char *s2 = NULL; + + s = NULL; /* for strtok_r() */ + + if (strncmp(tok, "eku=", sizeof("eku=") -1) == 0) { + /* + * Very simplistic handling of partial authz for EKUs: + * + * - denial of an EKU -> deny the whole request + * - else below mark all EKUs approved + */ + if (strstr(tok, ":denied")) { + krb5_set_error_message(context, EACCES, "CSR denied because " + "EKU denied: %s", tok); + ret = EACCES; + break; + } + continue; + } + + /* + * For SANs we check that the nth SAN in the response matches the nth + * SAN in the hx509_request. + */ + + if (strncmp(tok, "san_pkinit=", sizeof("san_pkinit=") - 1) == 0) { + tok += sizeof("san_pkinit=") - 1; + san_type = HX509_SAN_TYPE_PKINIT; + } else if (strncmp(tok, "san_dnsname=", sizeof("san_dnsname=") -1) == 0) { + tok += sizeof("san_dnsname=") - 1; + san_type = HX509_SAN_TYPE_DNSNAME; + } else if (strncmp(tok, "san_email=", sizeof("san_email=") -1) == 0) { + tok += sizeof("san_email=") - 1; + san_type = HX509_SAN_TYPE_EMAIL; + } else if (strncmp(tok, "san_xmpp=", sizeof("san_xmpp=") -1) == 0) { + tok += sizeof("san_xmpp=") - 1; + san_type = HX509_SAN_TYPE_XMPP; + } else if (strncmp(tok, "san_ms_upn=", sizeof("san_ms_upn=") -1) == 0) { + tok += sizeof("san_ms_upn=") - 1; + san_type = HX509_SAN_TYPE_MS_UPN; + } else { + krb5_set_error_message(context, EACCES, "CSR denied because could " + "not parse token in response: %s", tok); + ret = EACCES; + break; + } + + /* + * This token has to end in ":granted" or ":denied". Using our + * `strrpbrk()' means we can deal with principals names that have ':' + * in them. + */ + if ((p = strrpbrk(tok, ":")) == NULL) { + san_idx++; + continue; + } + *(p++) = '\0'; + + /* Now we get the nth SAN from the authorization */ + ret = hx509_request_get_san(csr, san_idx, &san_type2, &s2); + if (ret == HX509_NO_ITEM) { + /* See below */ + slow_path = 1; + break; + } + + /* And we check that it matches the SAN in this token */ + if (ret == 0) { + if (san_type != san_type2 || + strcmp(tok, s2) != 0) { + /* + * We expect the tokens in the reply to be in the same order as + * in the request. If not, we must take a slow path where we + * have to sort requests and responses then iterate them in + * order. + */ + slow_path = 1; + hx509_xfree(s2); + break; + } + hx509_xfree(s2); + + if (strcmp(p, "granted") == 0) { + ret = hx509_request_authorize_san(csr, san_idx); + } else { + partial = 1; + ret = hx509_request_reject_san(csr, san_idx); + } + if (ret) + break; + } + san_idx++; + } + + if (slow_path) { + /* + * FIXME? Implement the slow path? + * + * Basically, we'd get all the SANs from the request into an array of + * {SAN, index} and sort that array, then all the SANs from the + * response into an array and sort it, then step a cursor through both, + * using the index from the first to mark SANs in the request + * authorized or rejected. + */ + krb5_set_error_message(context, EACCES, "CSR denied because " + "authorizer service did not include all " + "piecemeal grants/denials in order"); + ret = EACCES; + } + + /* Mark all the EKUs authorized */ + for (eku_idx = 0; ret == 0; eku_idx++) + ret = hx509_request_authorize_eku(csr, eku_idx); + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret == 0 && partial) { + krb5_set_error_message(context, EACCES, "CSR partially authorized"); + ret = EACCES; + } + + free(rep2); + return ret; +} + +static krb5_error_code mark_authorized(hx509_request); + +static int +call_svc(krb5_context context, + heim_ipc ipc, + hx509_request csr, + const char *cmd, + int piecemeal_check_ok) +{ + heim_octet_string req, resp; + int ret; + + req.data = (void *)(uintptr_t)cmd; + req.length = strlen(cmd); + resp.length = 0; + resp.data = NULL; + ret = heim_ipc_call(ipc, &req, &resp, NULL); + + /* Check for all granted case */ + if (ret == 0 && + resp.length == sizeof("granted") - 1 && + strncasecmp(resp.data, "granted", sizeof("granted") - 1) == 0) { + free(resp.data); + return mark_authorized(csr); /* Full approval */ + } + + /* Check for "denied ..." piecemeal authorization case */ + if ((ret == 0 || ret == EACCES || ret == KRB5_PLUGIN_NO_HANDLE) && + piecemeal_check_ok && + resp.length > sizeof("denied") - 1 && + strncasecmp(resp.data, "denied", sizeof("denied") - 1) == 0) { + /* Piecemeal authorization */ + ret = mark_piecemeal_authorized(context, csr, &resp); + + /* mark_piecemeal_authorized() should return EACCES; just in case: */ + if (ret == 0) + ret = EACCES; + free(resp.data); + return ret; + } + + /* All other failure cases */ + + if (resp.data == NULL || resp.length == 0) { + krb5_set_error_message(context, ret, "CSR authorizer IPC service " + "failed silently"); + free(resp.data); + return EACCES; + } + + if (resp.length == sizeof("ignore") - 1 && + strncasecmp(resp.data, "ignore", sizeof("ignore") - 1) == 0) { + /* + * In this case the server is saying "I can't handle this request, try + * some other authorizer plugin". + */ + free(resp.data); + return KRB5_PLUGIN_NO_HANDLE; + } + + if (resp.length == sizeof("denied") - 1 && + strncasecmp(resp.data, "denied", sizeof("denied") - 1) == 0) { + krb5_set_error_message(context, ret, "CSR authorizer rejected %s", + cmd); + free(resp.data); + return EACCES; + } + + if (resp.length > INT_MAX) + krb5_set_error_message(context, ret, "CSR authorizer rejected %s", cmd); + else + krb5_set_error_message(context, ret, "CSR authorizer rejected %s: %.*s", + cmd, resp.length, resp.data); + + free(resp.data); + return ret; +} + +static void +frees(char **s) +{ + free(*s); + *s = NULL; +} + +static krb5_error_code +mark_authorized(hx509_request csr) +{ + size_t i; + char *s; + int ret = 0; + + for (i = 0; ret == 0; i++) { + ret = hx509_request_get_eku(csr, i, &s); + if (ret == 0) + hx509_request_authorize_eku(csr, i); + frees(&s); + } + if (ret == HX509_NO_ITEM) + ret = 0; + + for (i = 0; ret == 0; i++) { + hx509_san_type san_type; + ret = hx509_request_get_san(csr, i, &san_type, &s); + if (ret == 0) + hx509_request_authorize_san(csr, i); + frees(&s); + } + return ret == HX509_NO_ITEM ? 0 : ret; +} + +static KRB5_LIB_CALL krb5_error_code +authorize(void *ctx, + krb5_context context, + const char *app, + hx509_request csr, + krb5_const_principal client, + krb5_boolean *result) +{ + struct rk_strpool *cmd = NULL; + krb5_error_code ret; + hx509_context hx509ctx = NULL; + heim_ipc ipc = NULL; + const char *svc; + KeyUsage ku; + size_t i; + char *princ = NULL; + char *s = NULL; + int do_check = 0; + int piecemeal_check_ok = 1; + + if ((svc = krb5_config_get_string_default(context, NULL, + "ANY:org.h5l.csr_authorizer", + app ? app : "kdc", + "ipc_csr_authorizer", "service", + NULL)) == NULL) + return KRB5_PLUGIN_NO_HANDLE; + + if ((ret = heim_ipc_init_context(svc, &ipc))) { + /* + * If the IPC authorizer is optional, then fallback on whatever is + * next. + */ + if (krb5_config_get_bool_default(context, NULL, FALSE, + app ? app : "kdc", + "ipc_csr_authorizer", "optional", + NULL)) + return KRB5_PLUGIN_NO_HANDLE; + krb5_set_error_message(context, ret, "Could not set up IPC client " + "end-point for service %s", svc); + return ret; + } + + if ((ret = hx509_context_init(&hx509ctx))) + goto out; + + if ((ret = krb5_unparse_name(context, client, &princ))) + goto out; + + if ((ret = cmd_append(&cmd, "check ", princ, NULL))) + goto enomem; + frees(&princ); + + for (i = 0; ret == 0; i++) { + hx509_san_type san_type; + size_t p; + + ret = hx509_request_get_san(csr, i, &san_type, &s); + if (ret) + break; + + /* + * We cannot do a piecemeal check if any of the SANs could make the + * response ambiguous. + */ + p = strcspn(s, ",= "); + if (s[p] != '\0') + piecemeal_check_ok = 0; + if (piecemeal_check_ok && strstr(s, ":granted") != NULL) + piecemeal_check_ok = 0; + + switch (san_type) { + case HX509_SAN_TYPE_EMAIL: + if ((ret = cmd_append(&cmd, " san_email=", s, NULL))) + goto enomem; + do_check = 1; + break; + case HX509_SAN_TYPE_DNSNAME: + if ((ret = cmd_append(&cmd, " san_dnsname=", s, NULL))) + goto enomem; + do_check = 1; + break; + case HX509_SAN_TYPE_XMPP: + if ((ret = cmd_append(&cmd, " san_xmpp=", s, NULL))) + goto enomem; + do_check = 1; + break; + case HX509_SAN_TYPE_PKINIT: + if ((ret = cmd_append(&cmd, " san_pkinit=", s, NULL))) + goto enomem; + do_check = 1; + break; + case HX509_SAN_TYPE_MS_UPN: + if ((ret = cmd_append(&cmd, " san_ms_upn=", s, NULL))) + goto enomem; + do_check = 1; + break; + default: + if ((ret = hx509_request_reject_san(csr, i))) + goto out; + break; + } + frees(&s); + } + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret) + goto out; + + for (i = 0; ret == 0; i++) { + ret = hx509_request_get_eku(csr, i, &s); + if (ret) + break; + if ((ret = cmd_append(&cmd, " eku=", s, NULL))) + goto enomem; + do_check = 1; + frees(&s); + } + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret) + goto out; + + ku = int2KeyUsage(0); + ku.digitalSignature = 1; + ku.nonRepudiation = 1; + hx509_request_authorize_ku(csr, ku); + + if (do_check) { + s = rk_strpoolcollect(cmd); + cmd = NULL; + if (s == NULL) + goto enomem; + if ((ret = call_svc(context, ipc, csr, s, piecemeal_check_ok))) + goto out; + } /* else there was nothing to check -> permit */ + + *result = TRUE; + ret = 0; + goto out; + +enomem: + ret = krb5_enomem(context); + goto out; + +out: + heim_ipc_free_context(ipc); + hx509_context_free(&hx509ctx); + if (cmd) + rk_strpoolfree(cmd); + free(princ); + free(s); + return ret; +} + +static KRB5_LIB_CALL krb5_error_code +ipc_csr_authorizer_init(krb5_context context, void **c) +{ + *c = NULL; + return 0; +} + +static KRB5_LIB_CALL void +ipc_csr_authorizer_fini(void *c) +{ +} + +static krb5plugin_csr_authorizer_ftable plug_desc = + { 1, ipc_csr_authorizer_init, ipc_csr_authorizer_fini, authorize }; + +static krb5plugin_csr_authorizer_ftable *plugs[] = { &plug_desc }; + +static uintptr_t +ipc_csr_authorizer_get_instance(const char *libname) +{ + if (strcmp(libname, "krb5") == 0) + return krb5_get_instance(libname); + if (strcmp(libname, "kdc") == 0) + return kdc_get_instance(libname); + if (strcmp(libname, "hx509") == 0) + return hx509_get_instance(libname); + return 0; +} + +krb5_plugin_load_ft kdc_csr_authorizer_plugin_load; + +krb5_error_code KRB5_CALLCONV +kdc_csr_authorizer_plugin_load(heim_pcontext context, + krb5_get_instance_func_t *get_instance, + size_t *num_plugins, + krb5_plugin_common_ftable_cp **plugins) +{ + *get_instance = ipc_csr_authorizer_get_instance; + *num_plugins = sizeof(plugs) / sizeof(plugs[0]); + *plugins = (krb5_plugin_common_ftable_cp *)plugs; + return 0; +} diff --git a/third_party/heimdal/kdc/kdc-accessors.h b/third_party/heimdal/kdc/kdc-accessors.h new file mode 100644 index 0000000..c00fd49 --- /dev/null +++ b/third_party/heimdal/kdc/kdc-accessors.h @@ -0,0 +1,394 @@ +/* + * Copyright (c) 2022, PADL Software Pty Ltd. + * 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 PADL Software 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 PADL SOFTWARE 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 PADL SOFTWARE 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. + */ + +#ifndef HEIMDAL_KDC_KDC_ACCESSORS_H +#define HEIMDAL_KDC_KDC_ACCESSORS_H 1 + +/* read-only accessor */ +#ifndef _KDC_REQUEST_GET_ACCESSOR +#define _KDC_REQUEST_GET_ACCESSOR(R, T, f) \ + KDC_LIB_FUNCTION T KDC_LIB_CALL \ + kdc_request_get_ ## f(R); +#endif + +#ifndef _KDC_REQUEST_SET_ACCESSOR +#define _KDC_REQUEST_SET_ACCESSOR(R, T, f) \ + KDC_LIB_FUNCTION void KDC_LIB_CALL \ + kdc_request_set_ ## f(R, T); +#endif + +#ifndef KDC_REQUEST_GET_ACCESSOR +#define KDC_REQUEST_GET_ACCESSOR(T, f) \ + _KDC_REQUEST_GET_ACCESSOR(kdc_request_t, T, f) +#endif + +#ifndef KDC_REQUEST_SET_ACCESSOR +#define KDC_REQUEST_SET_ACCESSOR(T, f) \ + _KDC_REQUEST_SET_ACCESSOR(kdc_request_t, T, f) +#endif + +#ifndef ASTGS_REQUEST_GET_ACCESSOR +#define ASTGS_REQUEST_GET_ACCESSOR(T, f) \ + _KDC_REQUEST_GET_ACCESSOR(astgs_request_t, T, f) +#endif + +#ifndef ASTGS_REQUEST_SET_ACCESSOR +#define ASTGS_REQUEST_SET_ACCESSOR(T, f) \ + _KDC_REQUEST_SET_ACCESSOR(astgs_request_t, T, f) +#endif + +/* get/set accessor for pointer type */ +#ifndef _KDC_REQUEST_GET_ACCESSOR_PTR +#define _KDC_REQUEST_GET_ACCESSOR_PTR(R, T, f) \ + KDC_LIB_FUNCTION const T KDC_LIB_CALL \ + kdc_request_get_ ## f(R); +#endif + +#ifndef _KDC_REQUEST_SET_ACCESSOR_PTR +#define _KDC_REQUEST_SET_ACCESSOR_PTR(R, T, t, f) \ + KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL \ + kdc_request_set_ ## f(R, const T); +#endif + +#ifndef KDC_REQUEST_GET_ACCESSOR_PTR +#define KDC_REQUEST_GET_ACCESSOR_PTR(T, f) \ + _KDC_REQUEST_GET_ACCESSOR_PTR(kdc_request_t, T, f) +#endif + +#ifndef KDC_REQUEST_SET_ACCESSOR_PTR +#define KDC_REQUEST_SET_ACCESSOR_PTR(T, t, f) \ + _KDC_REQUEST_SET_ACCESSOR_PTR(kdc_request_t, T, t, f) +#endif + +#ifndef ASTGS_REQUEST_GET_ACCESSOR_PTR +#define ASTGS_REQUEST_GET_ACCESSOR_PTR(T, f) \ + _KDC_REQUEST_GET_ACCESSOR_PTR(astgs_request_t, T, f) +#endif + +#ifndef ASTGS_REQUEST_SET_ACCESSOR_PTR +#define ASTGS_REQUEST_SET_ACCESSOR_PTR(T, t, f) \ + _KDC_REQUEST_SET_ACCESSOR_PTR(astgs_request_t, T, t, f) +#endif + +/* get/set accessor for struct type */ +#ifndef _KDC_REQUEST_GET_ACCESSOR_STRUCT +#define _KDC_REQUEST_GET_ACCESSOR_STRUCT(R, T, f) \ + KDC_LIB_FUNCTION const T * KDC_LIB_CALL \ + kdc_request_get_ ## f(R); +#endif + +#ifndef _KDC_REQUEST_SET_ACCESSOR_STRUCT +#define _KDC_REQUEST_SET_ACCESSOR_STRUCT(R, T, t, f) \ + KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL \ + kdc_request_set_ ## f(R, const T *); +#endif + +#ifndef KDC_REQUEST_GET_ACCESSOR_STRUCT +#define KDC_REQUEST_GET_ACCESSOR_STRUCT(T, f) \ + _KDC_REQUEST_GET_ACCESSOR_STRUCT(kdc_request_t, T, f) +#endif + +#ifndef KDC_REQUEST_SET_ACCESSOR_STRUCT +#define KDC_REQUEST_SET_ACCESSOR_STRUCT(T, t, f) \ + _KDC_REQUEST_SET_ACCESSOR_STRUCT(kdc_request_t, T, t, f) +#endif + +#ifndef ASTGS_REQUEST_GET_ACCESSOR_STRUCT +#define ASTGS_REQUEST_GET_ACCESSOR_STRUCT(T, f) \ + _KDC_REQUEST_GET_ACCESSOR_STRUCT(astgs_request_t, T, f) +#endif + +#ifndef ASTGS_REQUEST_SET_ACCESSOR_STRUCT +#define ASTGS_REQUEST_SET_ACCESSOR_STRUCT(T, t, f) \ + _KDC_REQUEST_SET_ACCESSOR_STRUCT(astgs_request_t, T, t, f) +#endif + +/* + * krb5_context + * kdc_request_get_context(kdc_request_t); + */ + +KDC_REQUEST_GET_ACCESSOR(krb5_context, context) + +/* + * krb5_kdc_configuration * + * kdc_request_get_config(kdc_request_t); + */ + +KDC_REQUEST_GET_ACCESSOR(krb5_kdc_configuration *, config) + +/* + * heim_log_facility * + * kdc_request_get_logf(kdc_request_t); + */ + +KDC_REQUEST_GET_ACCESSOR(heim_log_facility *, logf) + +/* + * const char * + * kdc_request_get_from(kdc_request_t); + */ + +KDC_REQUEST_GET_ACCESSOR_PTR(char *, from) + +/* + * const struct sockaddr * + * kdc_request_get_addr(kdc_request_t); + */ + +KDC_REQUEST_GET_ACCESSOR_PTR(struct sockaddr *, addr) + +/* + * krb5_data + * kdc_request_get_request(kdc_request_t); + */ + +KDC_REQUEST_GET_ACCESSOR(krb5_data, request) + +/* + * struct timeval + * kdc_request_get_tv_start(kdc_request_t); + */ + +KDC_REQUEST_GET_ACCESSOR(struct timeval, tv_start) + +/* + * struct timeval + * kdc_request_get_tv_end(kdc_request_t); + */ + +KDC_REQUEST_GET_ACCESSOR(struct timeval, tv_end) + +/* + * krb5_error_code + * kdc_request_get_error_code(kdc_request_t); + */ +KDC_REQUEST_GET_ACCESSOR(krb5_error_code, error_code) + +/* + * void + * kdc_request_set_error_code(kdc_request_t, krb5_error_code); + */ +KDC_REQUEST_SET_ACCESSOR(krb5_error_code, error_code) + +/* + * const KDC_REQ * + * kdc_request_get_req(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_STRUCT(KDC_REQ, req) + +/* + * const KDC_REP * + * kdc_request_get_rep(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_STRUCT(KDC_REP, rep) + +/* + * krb5_error_code + * kdc_request_set_rep(astgs_request_t, const KDC_REP *); + */ + +ASTGS_REQUEST_SET_ACCESSOR_STRUCT(KDC_REP, KDC_REP, rep) + +/* + * const char * + * kdc_request_get_cname(kdc_request_t); + */ + +KDC_REQUEST_GET_ACCESSOR_PTR(char *, cname) + +/* + * krb5_error_code + * kdc_request_set_cname(kdc_request_t, const char *); + */ + +KDC_REQUEST_SET_ACCESSOR_PTR(char *, string_ptr, cname) + +/* + * const Principal * + * kdc_request_get_client_princ(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_PTR(Principal *, client_princ) + +/* + * krb5_error_code + * kdc_request_set_client_princ(astgs_request_t, const Principal *); + */ + +ASTGS_REQUEST_SET_ACCESSOR_PTR(Principal *, Principal_ptr, client_princ) + +/* + * const Principal * + * kdc_request_get_canon_client_princ(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_PTR(Principal *, canon_client_princ) + +/* + * krb5_error_code + * kdc_request_set_canon_client_princ(astgs_request_t, const Principal *); + */ + +ASTGS_REQUEST_SET_ACCESSOR_PTR(Principal *, Principal_ptr, canon_client_princ) + +/* + * const HDB * + * kdc_request_get_clientdb(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_PTR(HDB *, clientdb) + +/* + * const hdb_entry * + * kdc_request_get_client(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_PTR(hdb_entry *, client) + +/* + * See client accessors + */ + +KDC_REQUEST_GET_ACCESSOR_PTR(char *, sname) +KDC_REQUEST_SET_ACCESSOR_PTR(char *, string_ptr, sname) +ASTGS_REQUEST_GET_ACCESSOR_PTR(Principal *, server_princ) +ASTGS_REQUEST_SET_ACCESSOR_PTR(Principal *, Principal_ptr, server_princ) +ASTGS_REQUEST_GET_ACCESSOR_PTR(HDB *, serverdb) +ASTGS_REQUEST_GET_ACCESSOR_PTR(hdb_entry *, server) + +/* + * See client accessors + */ + +ASTGS_REQUEST_GET_ACCESSOR_PTR(Principal *, krbtgt_princ) +ASTGS_REQUEST_SET_ACCESSOR_PTR(Principal *, Principal_ptr, krbtgt_princ) +ASTGS_REQUEST_GET_ACCESSOR_PTR(HDB *, krbtgtdb) +ASTGS_REQUEST_GET_ACCESSOR_PTR(hdb_entry *, krbtgt) + +/* + * krb5_ticket * + * kdc_request_get_ticket(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR(krb5_ticket *, ticket) + +/* + * const krb5_keyblock * + * kdc_request_get_reply_key(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_STRUCT(krb5_keyblock, reply_key) + +/* + * krb5_error_code + * kdc_request_set_reply_key(astgs_request_t, const krb5_keyblock *); + */ + +ASTGS_REQUEST_SET_ACCESSOR_STRUCT(krb5_keyblock, keyblock, reply_key) + +/* + * krb5_const_pac + * kdc_request_get_pac(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_PTR(struct krb5_pac_data *, pac) + +/* + * krb5_error_code + * kdc_request_set_pac(astgs_request_t, krb5_const_pac); + */ + +ASTGS_REQUEST_SET_ACCESSOR_PTR(struct krb5_pac_data *, pac, pac) + +/* + * uint64_t + * kdc_request_get_pac_attributes(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR(uint64_t, pac_attributes) + +/* + * void + * kdc_request_set_pac_attributes(astgs_request_t, uint64_t); + */ + +ASTGS_REQUEST_SET_ACCESSOR(uint64_t, pac_attributes) + +KDC_LIB_FUNCTION const HDB * KDC_LIB_CALL +kdc_request_get_explicit_armor_clientdb(astgs_request_t); + +KDC_LIB_FUNCTION const hdb_entry * KDC_LIB_CALL +kdc_request_get_explicit_armor_client(astgs_request_t); + +KDC_LIB_FUNCTION const hdb_entry * KDC_LIB_CALL +kdc_request_get_explicit_armor_server(astgs_request_t); + +KDC_LIB_FUNCTION krb5_const_pac KDC_LIB_CALL +kdc_request_get_explicit_armor_pac(astgs_request_t); + +/* + * const HDB * + * kdc_request_get_armor_clientdb(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_PTR(HDB *, armor_clientdb) + +/* + * const hdb_entry * + * kdc_request_get_armor_client(astgs_request_t); + */ +ASTGS_REQUEST_GET_ACCESSOR_PTR(hdb_entry *, armor_client); + +/* + * const hdb_entry * + * kdc_request_get_armor_server(astgs_request_t); + */ +ASTGS_REQUEST_GET_ACCESSOR_PTR(hdb_entry *, armor_server); + +/* + * krb5_const_pac + * kdc_request_get_armor_pac(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_PTR(struct krb5_pac_data *, armor_pac); + +/* + * krb5_boolean + * kdc_request_get_explicit_armor_present(astgs_request_t); + */ + +ASTGS_REQUEST_GET_ACCESSOR_PTR(krb5_boolean, explicit_armor_present); + +#endif /* HEIMDAL_KDC_KDC_ACCESSORS_H */ diff --git a/third_party/heimdal/kdc/kdc-audit.h b/third_party/heimdal/kdc/kdc-audit.h new file mode 100644 index 0000000..4097c48 --- /dev/null +++ b/third_party/heimdal/kdc/kdc-audit.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2006 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. + */ + +/* $Id$ */ + +#ifndef HEIMDAL_KDC_KDC_AUDIT_H +#define HEIMDAL_KDC_KDC_AUDIT_H 1 + +/* + * KDC auditing + */ + +/* auth event type enumeration, currently for AS only */ +#define KDC_AUTH_EVENT_INVALID 0 /* no event logged */ +#define KDC_AUTH_EVENT_CLIENT_AUTHORIZED 1 /* all authn/authz checks passed */ +#define KDC_AUTH_EVENT_CLIENT_UNKNOWN 2 /* client unknown */ +#define KDC_AUTH_EVENT_CLIENT_LOCKED_OUT 3 /* client locked out */ +#define KDC_AUTH_EVENT_CLIENT_TIME_SKEW 4 /* client time skew */ +#define KDC_AUTH_EVENT_WRONG_LONG_TERM_KEY 5 /* PA failed to validate long term key */ +#define KDC_AUTH_EVENT_VALIDATED_LONG_TERM_KEY 6 /* PA validated long term key */ +#define KDC_AUTH_EVENT_CLIENT_NAME_UNAUTHORIZED 7 /* couldn't map GSS/PKINIT name to principal */ +#define KDC_AUTH_EVENT_PREAUTH_FAILED 8 /* generic PA failure */ +#define KDC_AUTH_EVENT_PREAUTH_SUCCEEDED 9 /* generic (non-long term key) PA success */ +#define KDC_AUTH_EVENT_HISTORIC_LONG_TERM_KEY 10 /* PA failed to validate current long term key, but historic */ +#define KDC_AUTH_EVENT_CLIENT_FOUND 11 /* the client was successfully looked up */ + +/* + * Audit keys to be queried using kdc_audit_getkv(). There are other keys + * intended for logging that are not defined below; the constants below are + * there to ease migration from the older auth_status HDB API. + */ + +#define KDC_REQUEST_KV_AUTH_EVENT "#auth_event" /* heim_number_t */ +#define KDC_REQUEST_KV_PA_NAME "pa" /* heim_string_t */ +#define KDC_REQUEST_KV_PA_ETYPE "pa-etype" /* heim_number_t */ +#define KDC_REQUEST_KV_PA_SUCCEEDED_KVNO "pa-succeeded-kvno" /* heim_number_t */ +#define KDC_REQUEST_KV_PA_FAILED_KVNO "pa-failed-kvno" /* heim_number_t */ +#define KDC_REQUEST_KV_GSS_INITIATOR "gss_initiator" /* heim_string_t */ +#define KDC_REQUEST_KV_PKINIT_CLIENT_CERT "pkinit_client_cert" /* heim_string_t */ +#define KDC_REQUEST_KV_PA_HISTORIC_KVNO "pa-historic-kvno" /* heim_number_t */ + +#endif /* HEIMDAL_KDC_KDC_AUDIT_H */ diff --git a/third_party/heimdal/kdc/kdc-plugin.c b/third_party/heimdal/kdc/kdc-plugin.c new file mode 100644 index 0000000..3b065c6 --- /dev/null +++ b/third_party/heimdal/kdc/kdc-plugin.c @@ -0,0 +1,753 @@ +/* + * Copyright (c) 2007 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions (c) 2021, 2022 PADL Software Pty Ltd. + * + * 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. + */ + +#include "kdc_locl.h" + +static int have_plugin = 0; + +/* + * Pick the first KDC plugin module that we find. + */ + +static const char *kdc_plugin_deps[] = { + "kdc", + "krb5", + "hdb", + NULL +}; + +static struct heim_plugin_data kdc_plugin_data = { + "krb5", + "kdc", + KRB5_PLUGIN_KDC_VERSION_11, + kdc_plugin_deps, + kdc_get_instance +}; + +static krb5_error_code KRB5_LIB_CALL +load(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + have_plugin = 1; + return KRB5_PLUGIN_NO_HANDLE; +} + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +krb5_kdc_plugin_init(krb5_context context) +{ + (void)_krb5_plugin_run_f(context, &kdc_plugin_data, 0, NULL, load); + + return 0; +} + +struct generate_uc { + astgs_request_t r; + hdb_entry *client; + hdb_entry *server; + const krb5_keyblock *reply_key; + uint64_t pac_attributes; + krb5_pac *pac; +}; + +static krb5_error_code KRB5_LIB_CALL +generate(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + const krb5plugin_kdc_ftable *ft = (const krb5plugin_kdc_ftable *)plug; + struct generate_uc *uc = (struct generate_uc *)userctx; + + if (ft->pac_generate == NULL) + return KRB5_PLUGIN_NO_HANDLE; + + return ft->pac_generate((void *)plug, + uc->r, + uc->client, + uc->server, + uc->reply_key, + uc->pac_attributes, + uc->pac); +} + + +krb5_error_code +_kdc_pac_generate(astgs_request_t r, + hdb_entry *client, + hdb_entry *server, + const krb5_keyblock *reply_key, + uint64_t pac_attributes, + krb5_pac *pac) +{ + krb5_error_code ret = 0; + struct generate_uc uc; + + *pac = NULL; + + if (krb5_config_get_bool_default(r->context, NULL, FALSE, "realms", + client->principal->realm, + "disable_pac", NULL)) + return 0; + + if (have_plugin) { + uc.r = r; + uc.client = client; + uc.server = server; + uc.reply_key = reply_key; + uc.pac = pac; + uc.pac_attributes = pac_attributes; + + ret = _krb5_plugin_run_f(r->context, &kdc_plugin_data, + 0, &uc, generate); + if (ret != KRB5_PLUGIN_NO_HANDLE) + return ret; + ret = 0; + } + + if (*pac == NULL) + ret = krb5_pac_init(r->context, pac); + + return ret; +} + +struct verify_uc { + astgs_request_t r; + krb5_const_principal client_principal; + hdb_entry *delegated_proxy; + hdb_entry *client; + hdb_entry *server; + hdb_entry *krbtgt; + EncTicketPart *ticket; + krb5_pac pac; +}; + +static krb5_error_code KRB5_LIB_CALL +verify(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + const krb5plugin_kdc_ftable *ft = (const krb5plugin_kdc_ftable *)plug; + struct verify_uc *uc = (struct verify_uc *)userctx; + krb5_error_code ret; + + if (ft->pac_verify == NULL) + return KRB5_PLUGIN_NO_HANDLE; + + ret = ft->pac_verify((void *)plug, + uc->r, + uc->client_principal, + uc->delegated_proxy, + uc->client, uc->server, uc->krbtgt, + uc->ticket, uc->pac); + return ret; +} + +krb5_error_code +_kdc_pac_verify(astgs_request_t r, + krb5_const_principal client_principal, + hdb_entry *delegated_proxy, + hdb_entry *client, + hdb_entry *server, + hdb_entry *krbtgt, + EncTicketPart *ticket, + krb5_pac pac) +{ + struct verify_uc uc; + + if (!have_plugin) + return KRB5_PLUGIN_NO_HANDLE; + + uc.r = r; + uc.client_principal = client_principal; + uc.delegated_proxy = delegated_proxy; + uc.client = client; + uc.server = server; + uc.krbtgt = krbtgt; + uc.ticket = ticket, + uc.pac = pac; + + return _krb5_plugin_run_f(r->context, &kdc_plugin_data, + 0, &uc, verify); +} + +struct update_uc { + astgs_request_t r; + krb5_const_principal client_principal; + hdb_entry *delegated_proxy; + krb5_const_pac delegated_proxy_pac; + hdb_entry *client; + hdb_entry *server; + hdb_entry *krbtgt; + krb5_pac *pac; +}; + +static krb5_error_code KRB5_LIB_CALL +update(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + const krb5plugin_kdc_ftable *ft = (const krb5plugin_kdc_ftable *)plug; + struct update_uc *uc = (struct update_uc *)userctx; + krb5_error_code ret; + + if (ft->pac_update == NULL) + return KRB5_PLUGIN_NO_HANDLE; + + ret = ft->pac_update((void *)plug, + uc->r, + uc->client_principal, + uc->delegated_proxy, + uc->delegated_proxy_pac, + uc->client, uc->server, uc->krbtgt, uc->pac); + return ret; +} + +krb5_error_code +_kdc_pac_update(astgs_request_t r, + krb5_const_principal client_principal, + hdb_entry *delegated_proxy, + krb5_const_pac delegated_proxy_pac, + hdb_entry *client, + hdb_entry *server, + hdb_entry *krbtgt, + krb5_pac *pac) +{ + struct update_uc uc; + + if (!have_plugin) + return KRB5_PLUGIN_NO_HANDLE; + + uc.r = r; + uc.client_principal = client_principal; + uc.delegated_proxy = delegated_proxy; + uc.delegated_proxy_pac = delegated_proxy_pac; + uc.client = client; + uc.server = server; + uc.krbtgt = krbtgt; + uc.pac = pac; + + return _krb5_plugin_run_f(r->context, &kdc_plugin_data, + 0, &uc, update); +} + +static krb5_error_code KRB5_LIB_CALL +check(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + const krb5plugin_kdc_ftable *ft = (const krb5plugin_kdc_ftable *)plug; + + if (ft->client_access == NULL) + return KRB5_PLUGIN_NO_HANDLE; + return ft->client_access((void *)plug, userctx); +} + +krb5_error_code +_kdc_check_access(astgs_request_t r) +{ + krb5_error_code ret = KRB5_PLUGIN_NO_HANDLE; + + if (have_plugin) { + ret = _krb5_plugin_run_f(r->context, &kdc_plugin_data, + 0, r, check); + } + + if (ret == KRB5_PLUGIN_NO_HANDLE) + return kdc_check_flags(r, r->req.msg_type == krb_as_req, + r->client, r->server); + return ret; +} + +static krb5_error_code KRB5_LIB_CALL +referral_policy(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + const krb5plugin_kdc_ftable *ft = (const krb5plugin_kdc_ftable *)plug; + + if (ft->referral_policy == NULL) + return KRB5_PLUGIN_NO_HANDLE; + return ft->referral_policy((void *)plug, userctx); +} + +krb5_error_code +_kdc_referral_policy(astgs_request_t r) +{ + krb5_error_code ret = KRB5_PLUGIN_NO_HANDLE; + + if (have_plugin) + ret = _krb5_plugin_run_f(r->context, &kdc_plugin_data, 0, r, referral_policy); + + return ret; +} + +static krb5_error_code KRB5_LIB_CALL +finalize_reply(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + const krb5plugin_kdc_ftable *ft = (const krb5plugin_kdc_ftable *)plug; + + if (ft->finalize_reply == NULL) + return KRB5_PLUGIN_NO_HANDLE; + return ft->finalize_reply((void *)plug, userctx); +} + +krb5_error_code +_kdc_finalize_reply(astgs_request_t r) +{ + krb5_error_code ret = KRB5_PLUGIN_NO_HANDLE; + + if (have_plugin) + ret = _krb5_plugin_run_f(r->context, &kdc_plugin_data, 0, r, finalize_reply); + + if (ret == KRB5_PLUGIN_NO_HANDLE) + ret = 0; + + return ret; +} + +static krb5_error_code KRB5_LIB_CALL +audit(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + const krb5plugin_kdc_ftable *ft = (const krb5plugin_kdc_ftable *)plug; + + if (ft->audit == NULL) + return KRB5_PLUGIN_NO_HANDLE; + return ft->audit((void *)plug, userctx); +} + +krb5_error_code +_kdc_plugin_audit(astgs_request_t r) +{ + krb5_error_code ret = KRB5_PLUGIN_NO_HANDLE; + + if (have_plugin) + ret = _krb5_plugin_run_f(r->context, &kdc_plugin_data, 0, r, audit); + + if (ret == KRB5_PLUGIN_NO_HANDLE) + ret = 0; + + return ret; +} + +KDC_LIB_FUNCTION uintptr_t KDC_LIB_CALL +kdc_get_instance(const char *libname) +{ + static const char *instance = "libkdc"; + + if (strcmp(libname, "kdc") == 0) + return (uintptr_t)instance; + else if (strcmp(libname, "hdb") == 0) + return hdb_get_instance(libname); + else if (strcmp(libname, "krb5") == 0) + return krb5_get_instance(libname); + + return 0; +} + +/* + * Minimum API surface wrapper for libheimbase object types so it + * may remain a private interface, yet plugins can interact with + * objects. + */ + +KDC_LIB_FUNCTION kdc_object_t KDC_LIB_CALL +kdc_object_alloc(size_t size, const char *name, kdc_type_dealloc dealloc) +{ + return heim_alloc(size, name, dealloc); +} + +KDC_LIB_FUNCTION kdc_object_t KDC_LIB_CALL +kdc_object_retain(kdc_object_t o) +{ + return heim_retain(o); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_object_release(kdc_object_t o) +{ + heim_release(o); +} + +KDC_LIB_FUNCTION kdc_object_t KDC_LIB_CALL +kdc_bool_create(krb5_boolean v) +{ + return heim_bool_create(v); +} + +KDC_LIB_FUNCTION krb5_boolean KDC_LIB_CALL +kdc_bool_get_value(kdc_object_t o) +{ + return heim_bool_val(o); +} + +struct kdc_array_iterator_trampoline_data { + kdc_array_iterator_t iter; + void *data; +}; + +/* + * Calling convention shim to avoid needing to update all internal + * consumers of heim_array_iterate_f() + */ +static void +_kdc_array_iterator_trampoline(kdc_object_t o, void *data, int *stop) +{ + struct kdc_array_iterator_trampoline_data *t = data; + + t->iter(o, t->data, stop); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_array_iterate(kdc_array_t a, void *d, kdc_array_iterator_t iter) +{ + struct kdc_array_iterator_trampoline_data t; + + t.iter = iter; + t.data = d; + + heim_array_iterate_f((heim_array_t)a, &t, _kdc_array_iterator_trampoline); +} + +KDC_LIB_FUNCTION size_t KDC_LIB_CALL +kdc_array_get_length(kdc_array_t a) +{ + return heim_array_get_length((heim_array_t)a); +} + +KDC_LIB_FUNCTION kdc_object_t KDC_LIB_CALL +kdc_array_get_value(heim_array_t a, size_t i) +{ + return heim_array_get_value((heim_array_t)a, i); +} + +KDC_LIB_FUNCTION kdc_object_t KDC_LIB_CALL +kdc_array_copy_value(heim_array_t a, size_t i) +{ + return heim_array_copy_value((heim_array_t)a, i); +} + +KDC_LIB_FUNCTION kdc_string_t KDC_LIB_CALL +kdc_string_create(const char *s) +{ + return (kdc_string_t)heim_string_create(s); +} + +KDC_LIB_FUNCTION const char * KDC_LIB_CALL +kdc_string_get_utf8(kdc_string_t s) +{ + return heim_string_get_utf8((heim_string_t)s); +} + +KDC_LIB_FUNCTION kdc_data_t +kdc_data_create(const void *d, size_t len) +{ + return (kdc_data_t)heim_data_create(d, len); +} + +KDC_LIB_FUNCTION const krb5_data * KDC_LIB_CALL +kdc_data_get_data(kdc_data_t d) +{ + return heim_data_get_data((heim_data_t)d); +} + +KDC_LIB_FUNCTION kdc_number_t KDC_LIB_CALL +kdc_number_create(int64_t v) +{ + return (kdc_number_t)heim_number_create(v); +} + +KDC_LIB_FUNCTION int64_t KDC_LIB_CALL +kdc_number_get_value(kdc_number_t n) +{ + return heim_number_get_long((heim_number_t)n); +} + +/* + * Plugin accessors + */ + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +kdc_request_add_reply_padata(astgs_request_t r, PA_DATA *md) +{ + heim_assert(r->rep.padata != NULL, "reply padata not allocated"); + return add_METHOD_DATA(r->rep.padata, md); +} + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +kdc_request_add_encrypted_padata(astgs_request_t r, PA_DATA *md) +{ + if (r->ek.encrypted_pa_data == NULL) { + r->ek.encrypted_pa_data = calloc(1, sizeof *(r->ek.encrypted_pa_data)); + if (r->ek.encrypted_pa_data == NULL) { + return ENOMEM; + } + } + + return add_METHOD_DATA(r->ek.encrypted_pa_data, md); +} + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +kdc_request_add_pac_buffer(astgs_request_t r, + uint32_t pactype, + const krb5_data *d) +{ + krb5_error_code ret; + krb5_pac pac; + + if (r->pac == NULL) { + ret = krb5_pac_init(r->context, &pac); + if (ret) + return ret; + } else + pac = heim_retain(r->pac); + + ret = krb5_pac_add_buffer(r->context, pac, pactype, d); + if (ret == 0 && r->pac == NULL) + r->pac = pac; + else + heim_release(pac); + + return ret; +} + +/* + * Override the e-data field to be returned in an error reply. The data will be + * owned by the KDC and eventually will be freed with krb5_data_free(). + */ +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +kdc_request_set_e_data(astgs_request_t r, heim_octet_string e_data) +{ + krb5_data_free(&r->e_data); + r->e_data = e_data; + + return 0; +} + +#undef _KDC_REQUEST_GET_ACCESSOR +#define _KDC_REQUEST_GET_ACCESSOR(R, T, f) \ + KDC_LIB_FUNCTION T KDC_LIB_CALL \ + kdc_request_get_ ## f(R r) \ + { \ + return r->f; \ + } + +#undef _KDC_REQUEST_SET_ACCESSOR +#define _KDC_REQUEST_SET_ACCESSOR(R, T, f) \ + KDC_LIB_FUNCTION void KDC_LIB_CALL \ + kdc_request_set_ ## f(R r, T v) \ + { \ + r->f = v; \ + } + +#undef _KDC_REQUEST_GET_ACCESSOR_PTR +#define _KDC_REQUEST_GET_ACCESSOR_PTR(R, T, f) \ + KDC_LIB_FUNCTION const T KDC_LIB_CALL \ + kdc_request_get_ ## f(R r) \ + { \ + return r->f; \ + } + +#undef _KDC_REQUEST_SET_ACCESSOR_PTR +#define _KDC_REQUEST_SET_ACCESSOR_PTR(R, T, t, f) \ + KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL \ + kdc_request_set_ ## f(R r, const T v) \ + { \ + krb5_error_code ret; \ + T tmp; \ + \ + if (v == r->f) \ + return 0; \ + else if (v) { \ + ret = copy_##t(v, &tmp); \ + if (ret) \ + return ret; \ + } else \ + tmp = NULL; \ + \ + free_##t(r->f); \ + r->f = tmp; \ + \ + return 0; \ + } + +#undef _KDC_REQUEST_GET_ACCESSOR_STRUCT +#define _KDC_REQUEST_GET_ACCESSOR_STRUCT(R, T, f) \ + KDC_LIB_FUNCTION const T * KDC_LIB_CALL \ + kdc_request_get_ ## f(R r) \ + { \ + return &r->f; \ + } + +#undef _KDC_REQUEST_SET_ACCESSOR_STRUCT +#define _KDC_REQUEST_SET_ACCESSOR_STRUCT(R, T, t, f) \ + KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL \ + kdc_request_set_ ## f(R r, const T *v) \ + { \ + krb5_error_code ret; \ + T tmp; \ + \ + if (v == NULL) \ + return EINVAL; \ + else if (v == &r->f) \ + return 0; \ + \ + ret = copy_##t(v, &tmp); \ + if (ret) \ + return ret; \ + \ + free_##t(&r->f); \ + r->f = tmp; \ + \ + return 0; \ + } + +static krb5_error_code +copy_string_ptr(const char *src, char **dst) +{ + *dst = strdup(src); + if (*dst == NULL) + return ENOMEM; + + return 0; +} + +static void +free_string_ptr(char *s) +{ + free(s); +} + +static krb5_error_code +copy_Principal_ptr(krb5_const_principal src, krb5_principal *dst) +{ + krb5_error_code ret; + krb5_principal p; + + *dst = NULL; + + p = calloc(1, sizeof(*p)); + if (p == NULL) + return ENOMEM; + + ret = copy_Principal(src, p); + if (ret == 0) + *dst = p; + else + free(p); + + return ret; +} + +static void +free_Principal_ptr(krb5_principal p) +{ + if (p) { + free_Principal(p); + free(p); + } +} + +static krb5_error_code +copy_pac(const struct krb5_pac_data *src, struct krb5_pac_data **dst) +{ + /* FIXME use heim_copy() when it exists */ + *dst = (krb5_pac)heim_retain((heim_object_t)src); + return 0; +} + +static void +free_pac(struct krb5_pac_data *o) +{ + heim_release(o); +} + +static krb5_error_code +copy_keyblock(const EncryptionKey *src, EncryptionKey *dst) +{ + return copy_EncryptionKey(src, dst); +} + +static void +free_keyblock(EncryptionKey *key) +{ + krb5_free_keyblock_contents(NULL, key); +} + +#undef HEIMDAL_KDC_KDC_ACCESSORS_H +#include "kdc-accessors.h" + +#undef _KDC_REQUEST_GET_ACCESSOR +#undef _KDC_REQUEST_SET_ACCESSOR + +#undef _KDC_REQUEST_GET_ACCESSOR_PTR +#undef _KDC_REQUEST_SET_ACCESSOR_PTR +#define _KDC_REQUEST_SET_ACCESSOR_PTR(R, T, t, f) \ + void \ + _kdc_request_set_ ## f ## _nocopy(R r, T *v) \ + { \ + if (*v != r->f) { \ + free_##t(r->f); \ + r->f = *v; \ + } \ + *v = NULL; \ + } + +#undef _KDC_REQUEST_GET_ACCESSOR_STRUCT +#undef _KDC_REQUEST_SET_ACCESSOR_STRUCT +#define _KDC_REQUEST_SET_ACCESSOR_STRUCT(R, T, t, f) \ + void \ + _kdc_request_set_ ## f ## _nocopy(R r, T *v) \ + { \ + if (v != &r->f) { \ + free_##t(&r->f); \ + r->f = *v; \ + } \ + memset(v, 0, sizeof(*v)); \ + } + +#undef HEIMDAL_KDC_KDC_ACCESSORS_H +#include "kdc-accessors.h" + +KDC_LIB_FUNCTION const HDB * KDC_LIB_CALL +kdc_request_get_explicit_armor_clientdb(astgs_request_t r) +{ + return r->explicit_armor_present ? r->armor_clientdb : NULL; +} + +KDC_LIB_FUNCTION const hdb_entry * KDC_LIB_CALL +kdc_request_get_explicit_armor_client(astgs_request_t r) +{ + return r->explicit_armor_present ? r->armor_client : NULL; +} + +KDC_LIB_FUNCTION const hdb_entry * KDC_LIB_CALL +kdc_request_get_explicit_armor_server(astgs_request_t r) +{ + return r->explicit_armor_present ? r->armor_server : NULL; +} + +KDC_LIB_FUNCTION krb5_const_pac KDC_LIB_CALL +kdc_request_get_explicit_armor_pac(astgs_request_t r) +{ + return r->explicit_armor_present ? r->armor_pac : NULL; +} diff --git a/third_party/heimdal/kdc/kdc-plugin.h b/third_party/heimdal/kdc/kdc-plugin.h new file mode 100644 index 0000000..5361349 --- /dev/null +++ b/third_party/heimdal/kdc/kdc-plugin.h @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2006 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. + */ + +/* $Id$ */ + +#ifndef HEIMDAL_KDC_KDC_PLUGIN_H +#define HEIMDAL_KDC_KDC_PLUGIN_H 1 + +#include <krb5.h> +#include <kdc.h> +#include <kdc-accessors.h> +#include <hdb.h> + +/* + * Allocate a PAC for the given client with krb5_pac_init(), + * and fill its contents in with krb5_pac_add_buffer(). + */ + +typedef krb5_error_code +(KRB5_CALLCONV *krb5plugin_kdc_pac_generate)(void *, + astgs_request_t, + hdb_entry *, /* client */ + hdb_entry *, /* server */ + const krb5_keyblock *, /* pk_replykey */ + uint64_t, /* pac_attributes */ + krb5_pac *); + +/* + * Verify the PAC KDC signatures by fetching the appropriate TGS key + * and calling krb5_pac_verify() with that key. + */ + +typedef krb5_error_code +(KRB5_CALLCONV *krb5plugin_kdc_pac_verify)(void *, + astgs_request_t, + krb5_const_principal, /* new ticket client */ + hdb_entry *, /* delegation proxy */ + hdb_entry *,/* client */ + hdb_entry *,/* server */ + hdb_entry *,/* krbtgt */ + EncTicketPart *, /* ticket */ + krb5_pac); /* pac */ + +/* + * Update the KDC PAC buffers. This function may be used after verifying the PAC + * with a call to krb5plugin_kdc_pac_verify(), and it resembles the latter + * function in the parameters it takes. The 'pac' parameter always points to a + * non-NULL PAC. + */ + +typedef krb5_error_code +(KRB5_CALLCONV *krb5plugin_kdc_pac_update)(void *, + astgs_request_t, + krb5_const_principal, /* new ticket client */ + hdb_entry *, /* delegation proxy */ + krb5_const_pac, /* delegation proxy pac */ + hdb_entry *,/* client */ + hdb_entry *,/* server */ + hdb_entry *,/* krbtgt */ + krb5_pac *); /* pac */ + +/* + * Authorize the client principal's access to the Authentication Service (AS). + * This function is called after any pre-authentication has completed. + */ + +typedef krb5_error_code +(KRB5_CALLCONV *krb5plugin_kdc_client_access)(void *, astgs_request_t); + +/* + * A referral policy plugin can either rewrite the server principal + * by resetting priv->server_princ, or it can disable referral + * processing entirely by returning an error. + * + * The error code from the previous server lookup is available as r->ret. + * + * If the function returns KRB5_PLUGIN_NO_HANDLE, the TGS will continue + * with its default referral handling. + * + * Note well: the plugin should free priv->server_princ is replacing. + */ + +typedef krb5_error_code +(KRB5_CALLCONV *krb5plugin_kdc_referral_policy)(void *, astgs_request_t); + +/* + * Update the AS or TGS reply immediately prior to encoding. + */ + +typedef krb5_error_code +(KRB5_CALLCONV *krb5plugin_kdc_finalize_reply)(void *, astgs_request_t); + +/* + * Audit an AS or TGS request. This function is called after encoding the + * reply (on success), or before encoding the error message. If a HDB audit + * function is also present, it is called after this one. + * + * The request should not be modified by the plugin. + */ + +typedef krb5_error_code +(KRB5_CALLCONV *krb5plugin_kdc_audit)(void *, astgs_request_t); + +/* + * Plugins should carefully check API contract notes for changes + * between plugin API versions. + */ +#define KRB5_PLUGIN_KDC_VERSION_11 11 + +typedef struct krb5plugin_kdc_ftable { + HEIM_PLUGIN_FTABLE_COMMON_ELEMENTS(krb5_context); + krb5plugin_kdc_pac_generate pac_generate; + krb5plugin_kdc_pac_verify pac_verify; + krb5plugin_kdc_pac_update pac_update; + krb5plugin_kdc_client_access client_access; + krb5plugin_kdc_referral_policy referral_policy; + krb5plugin_kdc_finalize_reply finalize_reply; + krb5plugin_kdc_audit audit; +} krb5plugin_kdc_ftable; + +#endif /* HEIMDAL_KDC_KDC_PLUGIN_H */ diff --git a/third_party/heimdal/kdc/kdc-replay.c b/third_party/heimdal/kdc/kdc-replay.c new file mode 100644 index 0000000..29190f7 --- /dev/null +++ b/third_party/heimdal/kdc/kdc-replay.c @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2007 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. + */ + +#include "kdc_locl.h" + +static int version_flag; +static int help_flag; + +struct getargs args[] = { + { "version", 0, arg_flag, &version_flag, NULL, NULL }, + { "help", 'h', arg_flag, &help_flag, NULL, NULL } +}; + +static const int num_args = sizeof(args) / sizeof(args[0]); + +static void +usage(int ret) +{ + arg_printusage (args, num_args, NULL, "kdc-request-log-file"); + exit (ret); +} + +int +main(int argc, char **argv) +{ + krb5_error_code ret; + krb5_context context; + krb5_kdc_configuration *config; + krb5_storage *sp; + int fd, optidx = 0; + + setprogname(argv[0]); + + if(getarg(args, num_args, argc, argv, &optidx)) + usage(1); + + if(help_flag) + usage(0); + + if(version_flag){ + print_version(NULL); + exit(0); + } + + ret = krb5_init_context(&context); + if (ret) + errx (1, "krb5_init_context failed to parse configuration file"); + + ret = krb5_kdc_get_config(context, &config); + if (ret) + krb5_err(context, 1, ret, "krb5_kdc_default_config"); + + kdc_openlog(context, "kdc-replay", config); + + ret = krb5_kdc_set_dbinfo(context, config); + if (ret) + krb5_err(context, 1, ret, "krb5_kdc_set_dbinfo"); + +#ifdef PKINIT + if (config->enable_pkinit) { + if (config->pkinit_kdc_identity == NULL) + krb5_errx(context, 1, "pkinit enabled but no identity"); + + if (config->pkinit_kdc_anchors == NULL) + krb5_errx(context, 1, "pkinit enabled but no X509 anchors"); + + krb5_kdc_pk_initialize(context, config, + config->pkinit_kdc_identity, + config->pkinit_kdc_anchors, + config->pkinit_kdc_cert_pool, + config->pkinit_kdc_revoke); + + } +#endif /* PKINIT */ + + if (argc != 2) + errx(1, "argc != 2"); + + printf("kdc replay\n"); + + fd = open(argv[1], O_RDONLY); + if (fd < 0) + err(1, "open: %s", argv[1]); + + sp = krb5_storage_from_fd(fd); + if (sp == NULL) + krb5_errx(context, 1, "krb5_storage_from_fd"); + + while(1) { + struct sockaddr_storage sa; + krb5_socklen_t salen = sizeof(sa); + struct timeval tv; + krb5_address a; + krb5_data d, r; + uint32_t t, clty, tag; + char astr[80]; + + ret = krb5_ret_uint32(sp, &t); + if (ret == HEIM_ERR_EOF) + break; + else if (ret) + krb5_errx(context, 1, "krb5_ret_uint32(version)"); + if (t != 1) + krb5_errx(context, 1, "version not 1"); + ret = krb5_ret_uint32(sp, &t); + if (ret) + krb5_errx(context, 1, "krb5_ret_uint32(time)"); + ret = krb5_ret_address(sp, &a); + if (ret) + krb5_errx(context, 1, "krb5_ret_address"); + ret = krb5_ret_data(sp, &d); + if (ret) + krb5_errx(context, 1, "krb5_ret_data"); + ret = krb5_ret_uint32(sp, &clty); + if (ret) + krb5_errx(context, 1, "krb5_ret_uint32(class|type)"); + ret = krb5_ret_uint32(sp, &tag); + if (ret) + krb5_errx(context, 1, "krb5_ret_uint32(tag)"); + + + ret = krb5_addr2sockaddr (context, &a, (struct sockaddr *)&sa, + &salen, 88); + if (ret == KRB5_PROG_ATYPE_NOSUPP) + goto out; + else if (ret) + krb5_err(context, 1, ret, "krb5_addr2sockaddr"); + + ret = krb5_print_address(&a, astr, sizeof(astr), NULL); + if (ret) + krb5_err(context, 1, ret, "krb5_print_address"); + + printf("processing request from %s, %lu bytes\n", + astr, (unsigned long)d.length); + + r.length = 0; + r.data = NULL; + + tv.tv_sec = t; + tv.tv_usec = 0; + + krb5_kdc_update_time(&tv); + krb5_set_real_time(context, tv.tv_sec, 0); + + ret = krb5_kdc_process_request(context, config, d.data, d.length, + &r, NULL, astr, + (struct sockaddr *)&sa, 0); + if (ret) + krb5_err(context, 1, ret, "krb5_kdc_process_request"); + + if (r.length) { + Der_class cl; + Der_type ty; + unsigned int tag2; + ret = der_get_tag (r.data, r.length, + &cl, &ty, &tag2, NULL); + if (ret) + krb5_err(context, 1, ret, "Could not decode replay data"); + if (MAKE_TAG(cl, ty, 0) != clty) + krb5_errx(context, 1, "class|type mismatch: %d != %d", + (int)MAKE_TAG(cl, ty, 0), (int)clty); + if (tag != tag2) + krb5_errx(context, 1, "tag mismatch"); + + krb5_data_free(&r); + } else { + if (clty != 0xffffffff) + krb5_errx(context, 1, "clty not invalid"); + if (tag != 0xffffffff) + krb5_errx(context, 1, "tag not invalid"); + } + + out: + krb5_data_free(&d); + krb5_free_address(context, &a); + } + + krb5_storage_free(sp); + krb5_free_context(context); + + printf("done\n"); + + return 0; +} diff --git a/third_party/heimdal/kdc/kdc-tester.c b/third_party/heimdal/kdc/kdc-tester.c new file mode 100644 index 0000000..8f8073a --- /dev/null +++ b/third_party/heimdal/kdc/kdc-tester.c @@ -0,0 +1,517 @@ +/* + * Copyright (c) 1997-2005 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2009 Apple Inc. 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. + */ + +#include "kdc_locl.h" +#include "send_to_kdc_plugin.h" + +struct perf { + unsigned long as_req; + unsigned long tgs_req; + struct timeval start; + struct timeval stop; + struct perf *next; +} *ptop; + +int detach_from_console = -1; +int daemon_child = -1; +int do_bonjour = -1; + +static krb5_kdc_configuration *kdc_config; +static krb5_context kdc_context; + +static struct sockaddr_storage sa; +static const char *astr = "0.0.0.0"; + +static void eval_object(heim_object_t); + + +/* + * + */ + +static krb5_error_code +plugin_init(krb5_context context, void **pctx) +{ + *pctx = NULL; + return 0; +} + +static void +plugin_fini(void *ctx) +{ +} + +static krb5_error_code +plugin_send_to_kdc(krb5_context context, + void *ctx, + krb5_krbhst_info *ho, + time_t timeout, + const krb5_data *in, + krb5_data *out) +{ + return KRB5_PLUGIN_NO_HANDLE; +} + +static krb5_error_code +plugin_send_to_realm(krb5_context context, + void *ctx, + krb5_const_realm realm, + time_t timeout, + const krb5_data *in, + krb5_data *out) +{ + int ret; + + krb5_kdc_update_time(NULL); + + ret = krb5_kdc_process_request(kdc_context, kdc_config, + in->data, in->length, + out, NULL, astr, + (struct sockaddr *)&sa, 0); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_kdc_process_request"); + + return 0; +} + +static krb5plugin_send_to_kdc_ftable send_to_kdc = { + KRB5_PLUGIN_SEND_TO_KDC_VERSION_2, + plugin_init, + plugin_fini, + plugin_send_to_kdc, + plugin_send_to_realm +}; + +static void +perf_start(struct perf *perf) +{ + memset(perf, 0, sizeof(*perf)); + + gettimeofday(&perf->start, NULL); + perf->next = ptop; + ptop = perf; +} + +static void +perf_stop(struct perf *perf) +{ + gettimeofday(&perf->stop, NULL); + ptop = perf->next; + + if (ptop) { + ptop->as_req += perf->as_req; + ptop->tgs_req += perf->tgs_req; + } + + timevalsub(&perf->stop, &perf->start); + printf("time: %lu.%06lu\n", + (unsigned long)perf->stop.tv_sec, + (unsigned long)perf->stop.tv_usec); + +#define USEC_PER_SEC 1000000 + + if (perf->as_req) { + double as_ps = 0.0; + as_ps = (perf->as_req * USEC_PER_SEC) / (double)((perf->stop.tv_sec * USEC_PER_SEC) + perf->stop.tv_usec); + printf("as-req/s %.2lf (total %lu requests)\n", as_ps, perf->as_req); + } + + if (perf->tgs_req) { + double tgs_ps = 0.0; + tgs_ps = (perf->tgs_req * USEC_PER_SEC) / (double)((perf->stop.tv_sec * USEC_PER_SEC) + perf->stop.tv_usec); + printf("tgs-req/s %.2lf (total %lu requests)\n", tgs_ps, perf->tgs_req); + } +} + +/* + * + */ + +static void +eval_repeat(heim_dict_t o) +{ + heim_object_t or = heim_dict_get_value(o, HSTR("value")); + heim_number_t n = heim_dict_get_value(o, HSTR("num")); + int i, num; + struct perf perf; + + perf_start(&perf); + + heim_assert(or != NULL, "value missing"); + heim_assert(n != NULL, "num missing"); + + num = heim_number_get_int(n); + heim_assert(num >= 0, "num >= 0"); + + for (i = 0; i < num; i++) + eval_object(or); + + perf_stop(&perf); +} + +/* + * + */ + +static krb5_error_code +copy_keytab(krb5_context context, krb5_keytab from, krb5_keytab to) +{ + krb5_keytab_entry entry; + krb5_kt_cursor cursor; + krb5_error_code ret; + + ret = krb5_kt_start_seq_get(context, from, &cursor); + if (ret) + return ret; + while ((ret = krb5_kt_next_entry(context, from, &entry, &cursor)) == 0){ + krb5_kt_add_entry(context, to, &entry); + krb5_kt_free_entry(context, &entry); + } + (void) krb5_kt_end_seq_get(context, from, &cursor); + if (ret == KRB5_KT_END) + return 0; + return ret; +} + +/* + * + */ + +static void +eval_kinit(heim_dict_t o) +{ + heim_string_t user, password, keytab, fast_armor_cc, pk_user_id, ccache; + krb5_get_init_creds_opt *opt; + krb5_init_creds_context ctx; + krb5_principal client; + krb5_keytab ktmem = NULL; + krb5_ccache fast_cc = NULL; + krb5_error_code ret; + + if (ptop) + ptop->as_req++; + + user = heim_dict_get_value(o, HSTR("client")); + if (user == NULL) + krb5_errx(kdc_context, 1, "no client"); + + password = heim_dict_get_value(o, HSTR("password")); + keytab = heim_dict_get_value(o, HSTR("keytab")); + pk_user_id = heim_dict_get_value(o, HSTR("pkinit-user-cert-id")); + if (password == NULL && keytab == NULL && pk_user_id == NULL) + krb5_errx(kdc_context, 1, "password, keytab, nor PKINIT user cert ID"); + + ccache = heim_dict_get_value(o, HSTR("ccache")); + + ret = krb5_parse_name(kdc_context, heim_string_get_utf8(user), &client); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_unparse_name"); + + /* PKINIT parts */ + ret = krb5_get_init_creds_opt_alloc (kdc_context, &opt); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_get_init_creds_opt_alloc"); + + if (pk_user_id) { + heim_bool_t rsaobj = heim_dict_get_value(o, HSTR("pkinit-use-rsa")); + int use_rsa = rsaobj ? heim_bool_val(rsaobj) : 0; + + ret = krb5_get_init_creds_opt_set_pkinit(kdc_context, opt, + client, + heim_string_get_utf8(pk_user_id), + NULL, NULL, NULL, + use_rsa ? 2 : 0, + NULL, NULL, NULL); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_get_init_creds_opt_set_pkinit"); + } + + fast_armor_cc = heim_dict_get_value(o, HSTR("fast-armor-cc")); + if (fast_armor_cc) { + + ret = krb5_cc_resolve(kdc_context, heim_string_get_utf8(fast_armor_cc), &fast_cc); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_cc_resolve"); + + ret = krb5_get_init_creds_opt_set_fast_ccache(kdc_context, opt, fast_cc); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_get_init_creds_set_fast_ccache"); + + ret = krb5_get_init_creds_opt_set_fast_flags(kdc_context, opt, KRB5_FAST_REQUIRED|KRB5_FAST_KDC_VERIFIED); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_get_init_creds_set_fast_ccache"); + + fast_cc = NULL; + } + + ret = krb5_init_creds_init(kdc_context, client, NULL, NULL, 0, opt, &ctx); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_init_creds_init"); + + if (password) { + ret = krb5_init_creds_set_password(kdc_context, ctx, + heim_string_get_utf8(password)); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_init_creds_set_password"); + } + if (keytab) { + krb5_keytab kt = NULL; + + ret = krb5_kt_resolve(kdc_context, heim_string_get_utf8(keytab), &kt); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_kt_resolve"); + + ret = krb5_kt_resolve(kdc_context, "MEMORY:keytab", &ktmem); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_kt_resolve(MEMORY)"); + + ret = copy_keytab(kdc_context, kt, ktmem); + if (ret) + krb5_err(kdc_context, 1, ret, "copy_keytab"); + + krb5_kt_close(kdc_context, kt); + + ret = krb5_init_creds_set_keytab(kdc_context, ctx, ktmem); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_init_creds_set_keytab"); + } + + ret = krb5_init_creds_get(kdc_context, ctx); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_init_creds_get"); + + if (ccache) { + const char *name = heim_string_get_utf8(ccache); + krb5_creds cred; + krb5_ccache cc; + + ret = krb5_init_creds_get_creds(kdc_context, ctx, &cred); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_init_creds_get_creds"); + + ret = krb5_cc_resolve(kdc_context, name, &cc); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_cc_resolve"); + + krb5_init_creds_store(kdc_context, ctx, cc); + + ret = krb5_cc_close(kdc_context, cc); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_cc_close"); + + krb5_free_cred_contents(kdc_context, &cred); + } + + krb5_init_creds_free(kdc_context, ctx); + + if (ktmem) + krb5_kt_close(kdc_context, ktmem); + if (fast_cc) + krb5_cc_close(kdc_context, fast_cc); +} + +/* + * + */ + +static void +eval_kgetcred(heim_dict_t o) +{ + heim_string_t server, ccache; + krb5_get_creds_opt opt; + heim_bool_t nostore; + krb5_error_code ret; + krb5_ccache cc = NULL; + krb5_principal s; + krb5_creds *out = NULL; + + if (ptop) + ptop->tgs_req++; + + server = heim_dict_get_value(o, HSTR("server")); + if (server == NULL) + krb5_errx(kdc_context, 1, "no server"); + + ccache = heim_dict_get_value(o, HSTR("ccache")); + if (ccache == NULL) + krb5_errx(kdc_context, 1, "no ccache"); + + nostore = heim_dict_get_value(o, HSTR("nostore")); + if (nostore == NULL) + nostore = heim_bool_create(1); + + ret = krb5_cc_resolve(kdc_context, heim_string_get_utf8(ccache), &cc); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_cc_resolve"); + + ret = krb5_parse_name(kdc_context, heim_string_get_utf8(server), &s); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_parse_name"); + + ret = krb5_get_creds_opt_alloc(kdc_context, &opt); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_get_creds_opt_alloc"); + + if (heim_bool_val(nostore)) + krb5_get_creds_opt_add_options(kdc_context, opt, KRB5_GC_NO_STORE); + + ret = krb5_get_creds(kdc_context, opt, cc, s, &out); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_get_creds"); + + krb5_free_creds(kdc_context, out); + krb5_free_principal(kdc_context, s); + krb5_get_creds_opt_free(kdc_context, opt); + krb5_cc_close(kdc_context, cc); +} + + +/* + * + */ + +static void +eval_kdestroy(heim_dict_t o) +{ + heim_string_t ccache = heim_dict_get_value(o, HSTR("ccache"));; + krb5_error_code ret; + const char *name; + krb5_ccache cc; + + heim_assert(ccache != NULL, "ccache_missing"); + + name = heim_string_get_utf8(ccache); + + ret = krb5_cc_resolve(kdc_context, name, &cc); + if (ret) + krb5_err(kdc_context, 1, ret, "krb5_cc_resolve"); + + krb5_cc_destroy(kdc_context, cc); +} + + +/* + * + */ + +static void +eval_array_element(heim_object_t o, void *ptr, int *stop) +{ + eval_object(o); +} + +static void +eval_object(heim_object_t o) +{ + heim_tid_t t = heim_get_tid(o); + + if (t == heim_array_get_type_id()) { + heim_array_iterate_f(o, NULL, eval_array_element); + } else if (t == heim_dict_get_type_id()) { + const char *op = heim_dict_get_value(o, HSTR("op")); + + heim_assert(op != NULL, "op missing"); + + if (strcmp(op, "repeat") == 0) { + eval_repeat(o); + } else if (strcmp(op, "kinit") == 0) { + eval_kinit(o); + } else if (strcmp(op, "kgetcred") == 0) { + eval_kgetcred(o); + } else if (strcmp(op, "kdestroy") == 0) { + eval_kdestroy(o); + } else { + errx(1, "unsupported ops %s", op); + } + + } else + errx(1, "unsupported"); +} + + +int +main(int argc, char **argv) +{ + krb5_error_code ret; + int optidx = 0; + + setprogname(argv[0]); + + ret = krb5_init_context(&kdc_context); + if (ret == KRB5_CONFIG_BADFORMAT) + errx (1, "krb5_init_context failed to parse configuration file"); + else if (ret) + errx (1, "krb5_init_context failed: %d", ret); + + ret = krb5_kt_register(kdc_context, &hdb_get_kt_ops); + if (ret) + errx (1, "krb5_kt_register(HDB) failed: %d", ret); + + kdc_config = configure(kdc_context, argc, argv, &optidx); + + argc -= optidx; + argv += optidx; + + if (argc == 0) + errx(1, "missing operations"); + + krb5_plugin_register(kdc_context, PLUGIN_TYPE_DATA, + KRB5_PLUGIN_SEND_TO_KDC, &send_to_kdc); + + { + void *buf; + size_t size; + heim_object_t o; + + if (rk_undumpdata(argv[0], &buf, &size)) + errx(1, "undumpdata: %s", argv[0]); + + o = heim_json_create_with_bytes(buf, size, 10, 0, NULL); + free(buf); + if (o == NULL) + errx(1, "heim_json"); + + /* + * do the work here + */ + + eval_object(o); + + heim_release(o); + } + + krb5_free_context(kdc_context); + return 0; +} diff --git a/third_party/heimdal/kdc/kdc-version.rc b/third_party/heimdal/kdc/kdc-version.rc new file mode 100644 index 0000000..662aff4 --- /dev/null +++ b/third_party/heimdal/kdc/kdc-version.rc @@ -0,0 +1,36 @@ +/*********************************************************************** + * Copyright (c) 2010, Secure Endpoints Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - 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. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 + * COPYRIGHT HOLDER 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 RC_FILE_TYPE VFT_APP +#define RC_FILE_DESC_0409 "Heimdal Kerberos v5 Server" +#define RC_FILE_ORIG_0409 "kdc.exe" + +#include "../windows/version.rc" diff --git a/third_party/heimdal/kdc/kdc.8 b/third_party/heimdal/kdc/kdc.8 new file mode 100644 index 0000000..150a3f1 --- /dev/null +++ b/third_party/heimdal/kdc/kdc.8 @@ -0,0 +1,217 @@ +.\" Copyright (c) 2003 - 2004 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. +.\" +.\" $Id$ +.\" +.Dd August 24, 2006 +.Dt KDC 8 +.Os HEIMDAL +.Sh NAME +.Nm kdc +.Nd Kerberos 5 server +.Sh SYNOPSIS +.Nm +.Bk -words +.Oo Fl c Ar file \*(Ba Xo +.Fl Fl config-file= Ns Ar file +.Xc +.Oc +.Op Fl p | Fl Fl no-require-preauth +.Op Fl Fl max-request= Ns Ar size +.Op Fl H | Fl Fl enable-http +.Oo Fl P Ar portspec \*(Ba Xo +.Fl Fl ports= Ns Ar portspec +.Xc +.Oc +.Op Fl Fl detach +.Op Fl Fl disable-des +.Op Fl Fl addresses= Ns Ar list of addresses +.Ek +.Sh DESCRIPTION +.Nm +serves requests for tickets. +When it starts, it first checks the flags passed, any options that are +not specified with a command line flag are taken from a config file, +or from a default compiled-in value. +.Pp +Options supported: +.Bl -tag -width Ds +.It Fl c Ar file , Fl Fl config-file= Ns Ar file +Specifies the location of the config file, the default is +.Pa /var/heimdal/kdc.conf . +This is the only value that can't be specified in the config file. +.It Fl p , Fl Fl no-require-preauth +Turn off the requirement for pre-autentication in the initial AS-REQ +for all principals. +The use of pre-authentication makes it more difficult to do offline +password attacks. +You might want to turn it off if you have clients +that don't support pre-authentication. +Since the version 4 protocol doesn't support any pre-authentication, +serving version 4 clients is just about the same as not requiring +pre-athentication. +The default is to require pre-authentication. +Adding the require-preauth per principal is a more flexible way of +handling this. +.It Fl Fl max-request= Ns Ar size +Gives an upper limit on the size of the requests that the kdc is +willing to handle. +.It Fl H , Fl Fl enable-http +Makes the kdc listen on port 80 and handle requests encapsulated in HTTP. +.It Fl P Ar portspec , Fl Fl ports= Ns Ar portspec +Specifies the set of ports the KDC should listen on. +It is given as a +white-space separated list of services or port numbers. +.It Fl Fl addresses= Ns Ar list of addresses +The list of addresses to listen for requests on. +By default, the kdc will listen on all the locally configured +addresses. +If only a subset is desired, or the automatic detection fails, this +option might be used. +.It Fl Fl detach +detach from pty and run as a daemon. +.It Fl Fl disable-des +disable all des encryption types, makes the kdc not use them. +.El +.Pp +All activities are logged to one or more destinations, see +.Xr krb5.conf 5 , +and +.Xr krb5_openlog 3 . +The entity used for logging is +.Nm kdc . +.Sh CONFIGURATION FILE +The configuration file has the same syntax as +.Xr krb5.conf 5 , +but will be read before +.Pa /etc/krb5.conf , +so it may override settings found there. +Options specific to the KDC only are found in the +.Dq [kdc] +section. +All the command-line options can preferably be added in the +configuration file. +The only difference is the pre-authentication flag, which has to be +specified as: +.Pp +.Dl require-preauth = no +.Pp +(in fact you can specify the option as +.Fl Fl require-preauth=no ) . +.Pp +And there are some configuration options which do not have +command-line equivalents: +.Bl -tag -width "xxx" -offset indent +.It Li enable-digest = Va boolean +turn on support for digest processing in the KDC. +The default is FALSE. +.It Li check-ticket-addresses = Va boolean +Check the addresses in the ticket when processing TGS requests. +The default is TRUE. +.It Li allow-null-ticket-addresses = Va boolean +Permit tickets with no addresses. +This option is only relevant when check-ticket-addresses is TRUE. +.It Li allow-anonymous = Va boolean +Permit anonymous tickets with no addresses. +.It Li historical_anon_realm = Va boolean +Enables pre-7.0 non-RFC-comformant KDC behavior. +With this option set to +.Li true +the client realm in anonymous pkinit AS replies will be the requested realm, +rather than the RFC-conformant +.Li WELLKNOWN:ANONYMOUS +realm. +This can have a security impact on servers that expect to grant access to +anonymous-but-authenticated to the KDC users of the realm in question: +they would also grant access to unauthenticated anonymous users. +As such, it is not recommend to set this option to +.Li true. +.It Li max-kdc-datagram-reply-length = Va number +Maximum packet size the UDP rely that the KDC will transmit, instead +the KDC sends back a reply telling the client to use TCP instead. +.It Li transited-policy = Li always-check \*(Ba \ +Li allow-per-principal | Li always-honour-request +This controls how KDC requests with the +.Li disable-transited-check +flag are handled. It can be one of: +.Bl -tag -width "xxx" -offset indent +.It Li always-check +Always check transited encoding, this is the default. +.It Li allow-per-principal +Currently this is identical to +.Li always-check . +In a future release, it will be possible to mark a principal as able +to handle unchecked requests. +.It Li always-honour-request +Always do what the client asked. +In a future release, it will be possible to force a check per +principal. +.El +.It encode_as_rep_as_tgs_rep = Va boolean +Encode AS-Rep as TGS-Rep to be bug-compatible with old DCE code. +The Heimdal clients allow both. +.It kdc_warn_pwexpire = Va time +How long before password/principal expiration the KDC should start +sending out warning messages. +.El +.Pp +The configuration file is only read when the +.Nm +is started. +If changes made to the configuration file are to take effect, the +.Nm +needs to be restarted. +.Pp +An example of a config file: +.Bd -literal -offset indent +[kdc] + require-preauth = no +.Ed +.Sh BUGS +If the machine running the KDC has new addresses added to it, the KDC +will have to be restarted to listen to them. +The reason it doesn't just listen to wildcarded (like INADDR_ANY) +addresses, is that the replies has to come from the same address they +were sent to, and most OS:es doesn't pass this information to the +application. +If your normal mode of operation require that you add and remove +addresses, the best option is probably to listen to a wildcarded TCP +socket, and make sure your clients use TCP to connect. +For instance, this will listen to IPv4 TCP port 88 only: +.Bd -literal -offset indent +kdc --addresses=0.0.0.0 --ports="88/tcp" +.Ed +.Pp +There should be a way to specify protocol, port, and address triplets, +not just addresses and protocol, port tuples. +.Sh SEE ALSO +.Xr kinit 1 , +.Xr krb5.conf 5 diff --git a/third_party/heimdal/kdc/kdc.h b/third_party/heimdal/kdc/kdc.h new file mode 100644 index 0000000..057d29a --- /dev/null +++ b/third_party/heimdal/kdc/kdc.h @@ -0,0 +1,138 @@ +/* + * Copyright (c) 1997-2022 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * + * Copyright (c) 2005 Andrew Bartlett <abartlet@samba.org> + * + * 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. + */ + +/* + * $Id$ + */ + +#ifndef __KDC_H__ +#define __KDC_H__ + +#include <hdb.h> +#include <krb5.h> +#include <kx509_asn1.h> +#include <gssapi/gssapi.h> + +enum krb5_kdc_trpolicy { + TRPOLICY_ALWAYS_CHECK, + TRPOLICY_ALLOW_PER_PRINCIPAL, + TRPOLICY_ALWAYS_HONOUR_REQUEST +}; + +struct krb5_kdc_configuration; +typedef struct krb5_kdc_configuration krb5_kdc_configuration; + +/* + * Access to request fields by plugins and other out-of-tree + * consumers should be via the functions in kdc-accessors.h. + */ + +struct kdc_request_desc; +typedef struct kdc_request_desc *kdc_request_t; + +struct astgs_request_desc; +typedef struct astgs_request_desc *astgs_request_t; + +struct kx509_req_context_desc; +typedef struct kx509_req_context_desc *kx509_req_context; + +struct krb5_kdc_service { + unsigned int flags; +#define KS_KRB5 1 +#define KS_NO_LENGTH 2 + const char *name; + krb5_error_code (*process)(kdc_request_t *, int *claim); +}; + +/* + * The following fields are guaranteed stable within a major + * release of Heimdal and can be manipulated by applications + * that manage KDC requests themselves using libkdc. + * + * Applications can make custom KDC configuration available + * to libkdc by using krb5_set_config(). + */ + +#define KRB5_KDC_CONFIGURATION_COMMON_ELEMENTS \ + krb5_log_facility *logf; \ + struct HDB **db; \ + size_t num_db; \ + const char *app; \ + \ + /* + * If non-null, contains static dummy data to include in + * place of the FAST cookie when it is disabled. + */ \ + krb5_data dummy_fast_cookie; \ + \ + /* \ + * Windows 2019 (and earlier versions) always sends the salt\ + * and Samba has testsuites that check this behaviour, so a \ + * Samba AD DC will set this flag to match the AS-REP packet\ + * exactly. \ + */ \ + unsigned int force_include_pa_etype_salt : 1; \ + \ + unsigned int tgt_use_strongest_session_key : 1; \ + unsigned int preauth_use_strongest_session_key : 1; \ + unsigned int svc_use_strongest_session_key : 1; \ + unsigned int use_strongest_server_key : 1; \ + \ + unsigned int require_pac : 1; \ + unsigned int enable_fast : 1; \ + unsigned int enable_fast_cookie : 1; \ + unsigned int enable_armored_pa_enc_timestamp : 1 + +#ifndef __KDC_LOCL_H__ +struct krb5_kdc_configuration { + KRB5_KDC_CONFIGURATION_COMMON_ELEMENTS; +}; +#endif + +typedef void *kdc_object_t; +typedef struct kdc_array_data *kdc_array_t; +typedef struct kdc_dict_data *kdc_dict_t; +typedef struct kdc_string_data *kdc_string_t; +typedef struct kdc_data_data *kdc_data_t; +typedef struct kdc_number_data *kdc_number_t; + +typedef void (KRB5_CALLCONV *kdc_array_iterator_t)(kdc_object_t, void *, int *); + +typedef void (KRB5_CALLCONV *kdc_type_dealloc)(kdc_object_t); + +#include <kdc-protos.h> + +#endif /* __KDC_H__ */ diff --git a/third_party/heimdal/kdc/kdc_locl.h b/third_party/heimdal/kdc/kdc_locl.h new file mode 100644 index 0000000..f2160b7 --- /dev/null +++ b/third_party/heimdal/kdc/kdc_locl.h @@ -0,0 +1,260 @@ +/* + * Copyright (c) 1997-2005 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. + */ + +/* + * $Id$ + */ + +#ifndef __KDC_LOCL_H__ +#define __KDC_LOCL_H__ + +#include "headers.h" + +typedef struct pk_client_params pk_client_params; +typedef struct gss_client_params gss_client_params; + +#include <kdc-private.h> + +#define FAST_EXPIRATION_TIME (3 * 60) + +/* KFE == KDC_FIND_ETYPE */ +#define KFE_IS_TGS 0x1 +#define KFE_IS_PREAUTH 0x2 +#define KFE_USE_CLIENT 0x4 + +#define heim_pcontext krb5_context +#define heim_pconfig krb5_kdc_configuration * +#include <heimbase-svc.h> + +#define KDC_AUDIT_EATWHITE HEIM_SVC_AUDIT_EATWHITE +#define KDC_AUDIT_VIS HEIM_SVC_AUDIT_VIS +#define KDC_AUDIT_VISLAST HEIM_SVC_AUDIT_VISLAST + +struct kdc_request_desc { + HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS; +}; + +struct kdc_patypes; + +struct krb5_kdc_configuration { + KRB5_KDC_CONFIGURATION_COMMON_ELEMENTS; + + int num_kdc_processes; + + size_t max_datagram_reply_length; + + time_t kdc_warn_pwexpire; /* time before expiration to print a warning */ + + unsigned int require_preauth : 1; /* require preauth for all principals */ + unsigned int encode_as_rep_as_tgs_rep : 1; /* bug compatibility */ + + unsigned int check_ticket_addresses : 1; + unsigned int warn_ticket_addresses : 1; + unsigned int allow_null_ticket_addresses : 1; + unsigned int allow_anonymous : 1; + unsigned int historical_anon_realm : 1; + unsigned int strict_nametypes : 1; + enum krb5_kdc_trpolicy trpolicy; + + unsigned int disable_pac : 1; + unsigned int enable_unarmored_pa_enc_timestamp : 1; + + unsigned int enable_pkinit : 1; + unsigned int require_pkinit_freshness : 1; + unsigned int pkinit_princ_in_cert : 1; + const char *pkinit_kdc_identity; + const char *pkinit_kdc_anchors; + const char *pkinit_kdc_friendly_name; + const char *pkinit_kdc_ocsp_file; + char **pkinit_kdc_cert_pool; + char **pkinit_kdc_revoke; + int pkinit_dh_min_bits; + unsigned int pkinit_require_binding : 1; + unsigned int pkinit_allow_proxy_certs : 1; + unsigned int synthetic_clients : 1; + unsigned int pkinit_max_life_from_cert_extension : 1; + krb5_timestamp pkinit_max_life_from_cert; + krb5_timestamp pkinit_max_life_bound; + krb5_timestamp synthetic_clients_max_life; + krb5_timestamp synthetic_clients_max_renew; + + int digests_allowed; + unsigned int enable_digest : 1; + + unsigned int enable_kx509 : 1; + + unsigned int enable_gss_preauth : 1; + unsigned int enable_gss_auth_data : 1; + gss_OID_set gss_mechanisms_allowed; + gss_OID_set gss_cross_realm_mechanisms_allowed; + +}; + +struct astgs_request_desc { + HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS; + + /* AS-REQ or TGS-REQ */ + KDC_REQ req; + + /* AS-REP or TGS-REP */ + KDC_REP rep; + EncTicketPart et; + EncKDCRepPart ek; + + /* client principal (AS) or TGT/S4U principal (TGS) */ + krb5_principal client_princ; + hdb_entry *client; + HDB *clientdb; + krb5_principal canon_client_princ; + + /* server principal */ + krb5_principal server_princ; + HDB *serverdb; + hdb_entry *server; + + /* presented ticket in TGS-REQ (unused by AS) */ + krb5_principal krbtgt_princ; + hdb_entry *krbtgt; + HDB *krbtgtdb; + krb5_ticket *ticket; + + krb5_keyblock reply_key; + + krb5_pac pac; + uint64_t pac_attributes; + + /* Only AS */ + const struct kdc_patypes *pa_used; + + /* PA methods can affect both the reply key and the session key (pkinit) */ + krb5_enctype sessionetype; + krb5_keyblock session_key; + + krb5_timestamp pa_endtime; + krb5_timestamp pa_max_life; + + krb5_keyblock strengthen_key; + const Key *ticket_key; + + /* only valid for tgs-req */ + unsigned int rk_is_subkey : 1; + unsigned int fast_asserted : 1; + unsigned int explicit_armor_present : 1; + krb5_keyblock enc_ad_key; + + krb5_crypto armor_crypto; + hdb_entry *armor_server; + HDB *armor_serverdb; + krb5_ticket *armor_ticket; + Key *armor_key; + + hdb_entry *armor_client; + HDB *armor_clientdb; + krb5_pac armor_pac; + + KDCFastState fast; +}; + +typedef struct kx509_req_context_desc { + HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS; + + struct Kx509Request req; + Kx509CSRPlus csr_plus; + krb5_auth_context ac; + const char *realm; /* XXX Confusion: is this crealm or srealm? */ + krb5_keyblock *key; + hx509_request csr; + krb5_times ticket_times; + unsigned int send_chain:1; /* Client expects a full chain */ + unsigned int have_csr:1; /* Client sent a CSR */ +} *kx509_req_context; + +#undef heim_pconfig +#undef heim_pcontext + +extern sig_atomic_t exit_flag; +extern size_t max_request_udp; +extern size_t max_request_tcp; +extern const char *request_log; +extern const char *port_str; +extern krb5_addresses explicit_addresses; + +extern int enable_http; + +extern int detach_from_console; +extern int daemon_child; +extern int do_bonjour; + +extern int testing_flag; + +extern const struct units _kdc_digestunits[]; + +#define KDC_LOG_FILE "kdc.log" + +extern struct timeval _kdc_now; +#define kdc_time (_kdc_now.tv_sec) + +extern char *runas_string; +extern char *chroot_string; + +void +start_kdc(krb5_context context, krb5_kdc_configuration *config, const char *argv0); + +krb5_kdc_configuration * +configure(krb5_context context, int argc, char **argv, int *optidx); + +#ifdef __APPLE__ +void bonjour_announce(krb5_context, krb5_kdc_configuration *); +#endif + +/* no-copy setters */ + +#undef _KDC_REQUEST_GET_ACCESSOR +#undef _KDC_REQUEST_SET_ACCESSOR + +#undef _KDC_REQUEST_GET_ACCESSOR_PTR +#undef _KDC_REQUEST_SET_ACCESSOR_PTR +#define _KDC_REQUEST_SET_ACCESSOR_PTR(R, T, t, f) \ + void \ + _kdc_request_set_ ## f ## _nocopy(R r, T *v); + +#undef _KDC_REQUEST_GET_ACCESSOR_STRUCT +#undef _KDC_REQUEST_SET_ACCESSOR_STRUCT +#define _KDC_REQUEST_SET_ACCESSOR_STRUCT(R, T, t, f) \ + void \ + _kdc_request_set_ ## f ## _nocopy(R r, T *v); + +#undef HEIMDAL_KDC_KDC_ACCESSORS_H +#include "kdc-accessors.h" + +#endif /* __KDC_LOCL_H__ */ diff --git a/third_party/heimdal/kdc/kerberos5.c b/third_party/heimdal/kdc/kerberos5.c new file mode 100644 index 0000000..5991711 --- /dev/null +++ b/third_party/heimdal/kdc/kerberos5.c @@ -0,0 +1,3247 @@ +/* + * Copyright (c) 1997-2007 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. + */ + +#include "kdc_locl.h" + +#ifdef TIME_T_SIGNED +#if SIZEOF_TIME_T == 4 +#define MAX_TIME ((time_t)INT32_MAX) +#elif SIZEOF_TIME_T == 8 +#define MAX_TIME ((time_t)INT64_MAX) +#else +#error "Unexpected sizeof(time_t)" +#endif +#else + +#if SIZEOF_TIME_T == 4 +#define MAX_TIME ((time_t)UINT32_MAX) +#else +#define MAX_TIME ((time_t)UINT64_MAX) +#endif +#endif + +#undef __attribute__ +#define __attribute__(X) + +void +_kdc_fix_time(time_t **t) +{ + if(*t == NULL){ + ALLOC(*t); + **t = MAX_TIME; + } + if(**t == 0) **t = MAX_TIME; /* fix for old clients */ +} + +static int +realloc_method_data(METHOD_DATA *md) +{ + PA_DATA *pa; + pa = realloc(md->val, (md->len + 1) * sizeof(*md->val)); + if(pa == NULL) + return ENOMEM; + md->val = pa; + md->len++; + return 0; +} + +static krb5_error_code +get_pa_etype_info2(krb5_context context, + krb5_kdc_configuration *config, + METHOD_DATA *md, Key *ckey, + krb5_boolean include_salt); + +static krb5_error_code +set_salt_padata(krb5_context context, + krb5_kdc_configuration *config, + METHOD_DATA *md, Key *key) +{ + if (!key->salt) + return 0; + + return get_pa_etype_info2(context, config, md, key, TRUE); +} + +const PA_DATA* +_kdc_find_padata(const KDC_REQ *req, int *start, int type) +{ + if (req->padata == NULL) + return NULL; + + while((size_t)*start < req->padata->len){ + (*start)++; + if(req->padata->val[*start - 1].padata_type == (unsigned)type) + return &req->padata->val[*start - 1]; + } + return NULL; +} + +/* + * This is a hack to allow predefined weak services, like afs to + * still use weak types + */ + +krb5_boolean +_kdc_is_weak_exception(krb5_principal principal, krb5_enctype etype) +{ + if (principal->name.name_string.len > 0 && + strcmp(principal->name.name_string.val[0], "afs") == 0 && + (etype == ETYPE_DES_CBC_CRC + || etype == ETYPE_DES_CBC_MD4 + || etype == ETYPE_DES_CBC_MD5)) + return TRUE; + return FALSE; +} + + +/* + * Detect if `key' is the using the the precomputed `default_salt'. + */ + +static krb5_boolean +is_default_salt_p(const krb5_salt *default_salt, const Key *key) +{ + if (key->salt == NULL) + return TRUE; + if (default_salt->salttype != key->salt->type) + return FALSE; + if (krb5_data_cmp(&default_salt->saltvalue, &key->salt->salt) != 0) + return FALSE; + return TRUE; +} + +/* + * Detect if `key' is the using the the precomputed `default_salt' + * (for des-cbc-crc) or any salt otherwise. + * + * This is for avoiding Kerberos v4 (yes really) keys in AS-REQ as + * that salt is strange, and a buggy client will try to use the + * principal as the salt and not the returned value. + */ + +static krb5_boolean +is_good_salt_p(const krb5_salt *default_salt, const Key *key) +{ + if (key->key.keytype == KRB5_ENCTYPE_DES_CBC_CRC) + return is_default_salt_p(default_salt, key); + + return TRUE; +} + +krb5_boolean +_kdc_is_anon_request(const KDC_REQ *req) +{ + const KDC_REQ_BODY *b = &req->req_body; + + /* + * Versions of Heimdal from 0.9rc1 through 1.50 use bit 14 instead + * of 16 for request_anonymous, as indicated in the anonymous draft + * prior to version 11. Bit 14 is assigned to S4U2Proxy, but S4U2Proxy + * requests are only sent to the TGS and, in any case, would have an + * additional ticket present. + */ + return b->kdc_options.request_anonymous || + (b->kdc_options.cname_in_addl_tkt && !b->additional_tickets); +} + +/* + * return the first appropriate key of `princ' in `ret_key'. Look for + * all the etypes in (`etypes', `len'), stopping as soon as we find + * one, but preferring one that has default salt. + * + * XXX This function does way way too much. Split it up! + * + * XXX `etypes' and `len' are always `b->etype.val' and `b->etype.len' -- the + * etype list from the KDC-REQ-BODY, which is available here as + * `r->req->req_body', so we could just stop having it passed in. + * + * XXX Picking an enctype(s) for PA-ETYPE-INFO* is rather different than + * picking an enctype for a ticket's session key. The former is what we do + * here when `(flags & KFE_IS_PREAUTH)', the latter otherwise. + */ + +krb5_error_code +_kdc_find_etype(astgs_request_t r, uint32_t flags, + krb5_enctype *etypes, unsigned len, + krb5_enctype *ret_enctype, Key **ret_key, + krb5_boolean *ret_default_salt) +{ + krb5_boolean use_strongest_session_key; + krb5_boolean is_preauth = flags & KFE_IS_PREAUTH; + krb5_boolean is_tgs = flags & KFE_IS_TGS; + hdb_entry *princ; + krb5_principal request_princ; + krb5_error_code ret; + krb5_salt def_salt; + krb5_enctype enctype = ETYPE_NULL; + const krb5_enctype *p; + Key *key = NULL; + size_t i, k, m; + + if (is_preauth && (flags & KFE_USE_CLIENT) && + r->client->flags.synthetic) + return KRB5KDC_ERR_ETYPE_NOSUPP; + + if ((flags & KFE_USE_CLIENT) && !r->client->flags.synthetic) { + princ = r->client; + request_princ = r->client_princ; + } else { + princ = r->server; + request_princ = r->server->principal; + } + + use_strongest_session_key = + is_preauth ? r->config->preauth_use_strongest_session_key + : (is_tgs ? r->config->tgt_use_strongest_session_key : + r->config->svc_use_strongest_session_key); + + /* We'll want to avoid keys with v4 salted keys in the pre-auth case... */ + ret = krb5_get_pw_salt(r->context, request_princ, &def_salt); + if (ret) + return ret; + + ret = KRB5KDC_ERR_ETYPE_NOSUPP; + + /* + * Pick an enctype that is in the intersection of: + * + * - permitted_enctypes (local policy) + * - requested enctypes (KDC-REQ-BODY's etype list) + * - the client's long-term keys' enctypes + * OR + * the server's configured etype list + * + * There are two sub-cases: + * + * - use local enctype preference (local policy) + * - use the client's preference list + */ + + if (use_strongest_session_key) { + /* + * Pick the strongest key that the KDC, target service, and + * client all support, using the local cryptosystem enctype + * list in strongest-to-weakest order to drive the search. + * + * This is not what RFC4120 says to do, but it encourages + * adoption of stronger enctypes. This doesn't play well with + * clients that have multiple Kerberos client implementations + * with different supported enctype lists sharing the same ccache. + */ + + /* drive the search with local supported enctypes list */ + p = krb5_kerberos_enctypes(r->context); + for (i = 0; + p[i] != ETYPE_NULL && enctype == ETYPE_NULL; + i++) { + if (krb5_enctype_valid(r->context, p[i]) != 0 && + !_kdc_is_weak_exception(princ->principal, p[i])) + continue; + + /* check that the client supports it too */ + for (k = 0; k < len && enctype == ETYPE_NULL; k++) { + + if (p[i] != etypes[k]) + continue; + + if (!is_preauth && (flags & KFE_USE_CLIENT)) { + /* + * It suffices that the client says it supports this + * enctype in its KDC-REQ-BODY's etype list, which is what + * `etypes' is here. + */ + enctype = p[i]; + ret = 0; + break; + } + + /* check target princ support */ + key = NULL; + if (!is_preauth && !(flags & KFE_USE_CLIENT) && princ->etypes) { + /* + * Use the etypes list from the server's HDB entry instead + * of deriving it from its long-term keys. This allows an + * entry to have just one long-term key but record support + * for multiple enctypes. + */ + for (m = 0; m < princ->etypes->len; m++) { + if (p[i] == princ->etypes->val[m]) { + enctype = p[i]; + ret = 0; + break; + } + } + } else { + /* + * Use the entry's long-term keys as the source of its + * supported enctypes, either because we're making + * PA-ETYPE-INFO* or because we're selecting a session key + * enctype. + */ + while (hdb_next_enctype2key(r->context, princ, NULL, + p[i], &key) == 0) { + if (key->key.keyvalue.length == 0) { + ret = KRB5KDC_ERR_NULL_KEY; + continue; + } + enctype = p[i]; + ret = 0; + if (is_preauth && ret_key != NULL && + !is_good_salt_p(&def_salt, key)) + continue; + } + } + } + } + } else { + /* + * Pick the first key from the client's enctype list that is + * supported by the cryptosystem and by the given principal. + * + * RFC4120 says we SHOULD pick the first _strong_ key from the + * client's list... not the first key... If the admin disallows + * weak enctypes in krb5.conf and selects this key selection + * algorithm, then we get exactly what RFC4120 says. + */ + for(i = 0; ret != 0 && i < len; i++) { + + if (krb5_enctype_valid(r->context, etypes[i]) != 0 && + !_kdc_is_weak_exception(princ->principal, etypes[i])) + continue; + + key = NULL; + while (ret != 0 && + hdb_next_enctype2key(r->context, princ, NULL, + etypes[i], &key) == 0) { + if (key->key.keyvalue.length == 0) { + ret = KRB5KDC_ERR_NULL_KEY; + continue; + } + enctype = etypes[i]; + ret = 0; + if (is_preauth && ret_key != NULL && + !is_good_salt_p(&def_salt, key)) + continue; + } + } + } + + if (ret == 0 && enctype == ETYPE_NULL) { + /* + * if the service principal is one for which there is a known 1DES + * exception and no other enctype matches both the client request and + * the service key list, provide a DES-CBC-CRC key. + */ + if (ret_key == NULL && + _kdc_is_weak_exception(princ->principal, ETYPE_DES_CBC_CRC)) { + ret = 0; + enctype = ETYPE_DES_CBC_CRC; + } else { + ret = KRB5KDC_ERR_ETYPE_NOSUPP; + } + } + + if (ret == 0) { + if (ret_enctype != NULL) + *ret_enctype = enctype; + if (ret_key != NULL) + *ret_key = key; + if (ret_default_salt != NULL) + *ret_default_salt = is_default_salt_p(&def_salt, key); + } + + krb5_free_salt (r->context, def_salt); + return ret; +} + +/* + * The principal's session_etypes must be sorted in order of strength, with + * preferred etype first. +*/ +krb5_error_code +_kdc_find_session_etype(astgs_request_t r, + krb5_enctype *etypes, size_t len, + const hdb_entry *princ, + krb5_enctype *ret_enctype) +{ + size_t i; + + if (princ->session_etypes == NULL) { + /* The principal must have session etypes available. */ + return KRB5KDC_ERR_ETYPE_NOSUPP; + } + + /* Loop over the client's specified etypes. */ + for (i = 0; i < len; ++i) { + size_t j; + + /* Check that the server also supports the etype. */ + for (j = 0; j < princ->session_etypes->len; ++j) { + if (princ->session_etypes->val[j] == etypes[i]) { + *ret_enctype = etypes[i]; + return 0; + } + } + } + + return KRB5KDC_ERR_ETYPE_NOSUPP; +} + +krb5_error_code +_kdc_make_anonymous_principalname (PrincipalName *pn) +{ + pn->name_type = KRB5_NT_WELLKNOWN; + pn->name_string.len = 2; + pn->name_string.val = calloc(2, sizeof(*pn->name_string.val)); + if (pn->name_string.val == NULL) + goto failed; + + pn->name_string.val[0] = strdup(KRB5_WELLKNOWN_NAME); + if (pn->name_string.val[0] == NULL) + goto failed; + + pn->name_string.val[1] = strdup(KRB5_ANON_NAME); + if (pn->name_string.val[1] == NULL) + goto failed; + + return 0; + +failed: + free_PrincipalName(pn); + + pn->name_type = KRB5_NT_UNKNOWN; + pn->name_string.len = 0; + pn->name_string.val = NULL; + + return ENOMEM; +} + +static void +_kdc_r_log(astgs_request_t r, int level, const char *fmt, ...) + __attribute__ ((__format__ (__printf__, 3, 4))) +{ + va_list ap; + char *s; + va_start(ap, fmt); + s = kdc_log_msg_va(r->context, r->config, level, fmt, ap); + if(s) free(s); + va_end(ap); +} + +void +_kdc_set_const_e_text(astgs_request_t r, const char *e_text) +{ + /* We should never see this */ + if (r->e_text) { + kdc_log(r->context, r->config, 1, + "trying to replace e-text \"%s\" with \"%s\"\n", + r->e_text, e_text); + return; + } + + r->e_text = e_text; + kdc_log(r->context, r->config, 4, "%s", e_text); +} + +void +_kdc_set_e_text(astgs_request_t r, const char *fmt, ...) + __attribute__ ((__format__ (__printf__, 2, 3))) +{ + va_list ap; + char *e_text = NULL; + int vasprintf_ret; + + va_start(ap, fmt); + vasprintf_ret = vasprintf(&e_text, fmt, ap); + va_end(ap); + + if (vasprintf_ret < 0 || !e_text) { + /* not much else to do... */ + kdc_log(r->context, r->config, 1, + "Could not set e_text: %s (out of memory)", fmt); + return; + } + + /* We should never see this */ + if (r->e_text) { + kdc_log(r->context, r->config, 1, "trying to replace e-text: %s\n", + e_text); + free(e_text); + return; + } + + r->e_text = e_text; + r->e_text_buf = e_text; + kdc_log(r->context, r->config, 4, "%s", e_text); +} + +void +_kdc_log_timestamp(astgs_request_t r, const char *type, + KerberosTime authtime, KerberosTime *starttime, + KerberosTime endtime, KerberosTime *renew_till) +{ + krb5_kdc_configuration *config = r->config; + char authtime_str[100], starttime_str[100], + endtime_str[100], renewtime_str[100]; + + if (authtime) + kdc_audit_setkv_number((kdc_request_t)r, "auth", authtime); + if (starttime && *starttime) + kdc_audit_setkv_number((kdc_request_t)r, "start", *starttime); + if (endtime) + kdc_audit_setkv_number((kdc_request_t)r, "end", endtime); + if (renew_till && *renew_till) + kdc_audit_setkv_number((kdc_request_t)r, "renew", *renew_till); + + krb5_format_time(r->context, authtime, + authtime_str, sizeof(authtime_str), TRUE); + if (starttime) + krb5_format_time(r->context, *starttime, + starttime_str, sizeof(starttime_str), TRUE); + else + strlcpy(starttime_str, "unset", sizeof(starttime_str)); + krb5_format_time(r->context, endtime, + endtime_str, sizeof(endtime_str), TRUE); + if (renew_till) + krb5_format_time(r->context, *renew_till, + renewtime_str, sizeof(renewtime_str), TRUE); + else + strlcpy(renewtime_str, "unset", sizeof(renewtime_str)); + + kdc_log(r->context, config, 4, + "%s authtime: %s starttime: %s endtime: %s renew till: %s", + type, authtime_str, starttime_str, endtime_str, renewtime_str); +} + +/* + * + */ + +#ifdef PKINIT + +static krb5_error_code +pa_pkinit_validate(astgs_request_t r, const PA_DATA *pa) +{ + pk_client_params *pkp = NULL; + char *client_cert = NULL; + krb5_error_code ret; + + ret = _kdc_pk_rd_padata(r, pa, &pkp); + if (ret || pkp == NULL) { + if (ret == HX509_CERT_REVOKED) { + ret = KRB5_KDC_ERR_CLIENT_NOT_TRUSTED; + } else { + ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; + } + _kdc_r_log(r, 4, "Failed to decode PKINIT PA-DATA -- %s", + r->cname); + goto out; + } + + /* Validate the freshness token. */ + ret = _kdc_pk_validate_freshness_token(r, pkp); + if (ret) { + _kdc_r_log(r, 4, "Failed to validate freshness token"); + goto out; + } + + ret = _kdc_pk_check_client(r, pkp, &client_cert); + if (client_cert) + kdc_audit_addkv((kdc_request_t)r, 0, KDC_REQUEST_KV_PKINIT_CLIENT_CERT, + "%s", client_cert); + if (ret) { + _kdc_set_e_text(r, "PKINIT certificate not allowed to " + "impersonate principal"); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_CLIENT_NAME_UNAUTHORIZED); + goto out; + } + + r->pa_endtime = _kdc_pk_endtime(pkp); + if (!r->client->flags.synthetic) + r->pa_max_life = _kdc_pk_max_life(pkp); + + _kdc_r_log(r, 4, "PKINIT pre-authentication succeeded -- %s using %s", + r->cname, client_cert); + + ret = _kdc_pk_mk_pa_reply(r, pkp); + if (ret) { + _kdc_set_e_text(r, "Failed to build PK-INIT reply"); + goto out; + } + ret = _kdc_add_initial_verified_cas(r->context, r->config, + pkp, &r->et); + + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_PREAUTH_SUCCEEDED); + + /* + * Match Windows by preferring the authenticator nonce over the one in the + * request body. + */ + r->ek.nonce = _kdc_pk_nonce(pkp); + + out: + if (pkp) + _kdc_pk_free_client_param(r->context, pkp); + free(client_cert); + + return ret; +} + +#endif /* PKINIT */ + +static krb5_error_code +pa_gss_validate(astgs_request_t r, const PA_DATA *pa) +{ + gss_client_params *gcp = NULL; + char *client_name = NULL; + krb5_error_code ret; + int open = 0; + + ret = _kdc_gss_rd_padata(r, pa, &gcp, &open); + if (ret && gcp == NULL) + return ret; + + if (open) { + ret = _kdc_gss_check_client(r, gcp, &client_name); + if (client_name) + kdc_audit_addkv((kdc_request_t)r, 0, KDC_REQUEST_KV_GSS_INITIATOR, + "%s", client_name); + if (ret) { + _kdc_set_e_text(r, "GSS-API client not allowed to " + "impersonate principal"); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_CLIENT_NAME_UNAUTHORIZED); + goto out; + } + + r->pa_endtime = _kdc_gss_endtime(r, gcp); + + _kdc_r_log(r, 4, "GSS pre-authentication succeeded -- %s using %s", + r->cname, client_name); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_PREAUTH_SUCCEEDED); + + ret = _kdc_gss_mk_composite_name_ad(r, gcp); + if (ret) { + _kdc_set_e_text(r, "Failed to build GSS authorization data"); + goto out; + } + } + + ret = _kdc_gss_mk_pa_reply(r, gcp); + if (ret) { + if (ret != KRB5_KDC_ERR_MORE_PREAUTH_DATA_REQUIRED) + _kdc_set_e_text(r, "Failed to build GSS pre-authentication reply"); + goto out; + } + + ret = kdc_request_set_attribute((kdc_request_t)r, + HSTR("org.h5l.pa-gss-client-params"), gcp); + if (ret) + goto out; + +out: + kdc_object_release(gcp); + free(client_name); + + return ret; +} + +static krb5_error_code +pa_gss_finalize_pac(astgs_request_t r) +{ + gss_client_params *gcp; + + gcp = kdc_request_get_attribute((kdc_request_t)r, HSTR("org.h5l.pa-gss-client-params")); + + heim_assert(gcp != NULL, "invalid GSS-API client params"); + + return _kdc_gss_finalize_pac(r, gcp); +} + +static krb5_error_code +pa_enc_chal_decrypt_kvno(astgs_request_t r, + krb5_enctype aenctype, + krb5_data *pepper1client, + krb5_data *pepper1kdc, + krb5_data *pepper2, + krb5_kvno kvno, + EncryptedData *enc_data, + krb5_keyblock *KDCchallengekey, + struct Key **used_key) +{ + unsigned int invalidKeys = 0; + krb5_error_code ret; + const Keys *keys = NULL; + unsigned int i; + + if (KDCchallengekey) + krb5_keyblock_zero(KDCchallengekey); + if (used_key) + *used_key = NULL; + + keys = hdb_kvno2keys(r->context, r->client, kvno); + if (keys == NULL) { + return KRB5KDC_ERR_ETYPE_NOSUPP; + } + + for (i = 0; i < keys->len; i++) { + struct Key *k = &keys->val[i]; + krb5_crypto challengecrypto, longtermcrypto; + krb5_keyblock client_challengekey; + + ret = krb5_crypto_init(r->context, &k->key, 0, &longtermcrypto); + if (ret) + continue; + + ret = krb5_crypto_fx_cf2(r->context, r->armor_crypto, longtermcrypto, + pepper1client, pepper2, aenctype, + &client_challengekey); + if (ret) { + krb5_crypto_destroy(r->context, longtermcrypto); + continue; + } + + ret = krb5_crypto_init(r->context, &client_challengekey, 0, + &challengecrypto); + krb5_free_keyblock_contents(r->context, &client_challengekey); + if (ret) { + krb5_crypto_destroy(r->context, longtermcrypto); + continue; + } + + ret = _krb5_validate_pa_enc_challenge(r->context, + challengecrypto, + KRB5_KU_ENC_CHALLENGE_CLIENT, + enc_data, + r->cname); + krb5_crypto_destroy(r->context, challengecrypto); + if (ret) { + const char *msg; + krb5_error_code ret2; + char *str = NULL; + + krb5_crypto_destroy(r->context, longtermcrypto); + + if (ret != KRB5KRB_AP_ERR_BAD_INTEGRITY) + return ret; + + invalidKeys += 1; + + if (pepper1kdc == NULL) + /* The caller is not interessted in details */ + continue; + + ret2 = krb5_enctype_to_string(r->context, k->key.keytype, &str); + if (ret2) + str = NULL; + msg = krb5_get_error_message(r->context, ret); + _kdc_r_log(r, 2, "Failed to decrypt ENC-CHAL -- %s " + "(enctype %s) error %s", + r->cname, str ? str : "unknown enctype", msg); + krb5_free_error_message(r->context, msg); + free(str); + + continue; + } + + if (pepper1kdc == NULL) { + /* The caller is not interessted in details */ + return 0; + } + + heim_assert(KDCchallengekey != NULL, + "KDCchallengekey pointer required with pepper1kdc"); + heim_assert(used_key != NULL, + "used_key pointer required with pepper1kdc"); + + /* + * Provide KDC authentication to the client, uses a different + * challenge key (different pepper). + */ + + ret = krb5_crypto_fx_cf2(r->context, r->armor_crypto, longtermcrypto, + pepper1kdc, pepper2, aenctype, + KDCchallengekey); + krb5_crypto_destroy(r->context, longtermcrypto); + if (ret) + return ret; + + *used_key = k; + return 0; + } + + if (invalidKeys == 0) + return KRB5KDC_ERR_ETYPE_NOSUPP; + + return KRB5KDC_ERR_PREAUTH_FAILED; +} + +static krb5_error_code +pa_enc_chal_validate(astgs_request_t r, const PA_DATA *pa) +{ + krb5_kvno kvno = r->client->kvno; + krb5_data pepper1client, pepper1kdc, pepper2; + EncryptedData enc_data; + krb5_enctype aenctype; + krb5_error_code ret; + krb5_keyblock KDCchallengekey; + struct Key *k = NULL; + size_t size; + + heim_assert(r->armor_crypto != NULL, "ENC-CHAL called for non FAST"); + + if (_kdc_is_anon_request(&r->req)) { + ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; + kdc_log(r->context, r->config, 4, "ENC-CHAL doesn't support anon"); + return ret; + } + + if (r->client->flags.locked_out) { + ret = KRB5KDC_ERR_CLIENT_REVOKED; + kdc_log(r->context, r->config, 0, + "Client (%s) is locked out", r->cname); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_CLIENT_LOCKED_OUT); + return ret; + } + + ret = decode_EncryptedData(pa->padata_value.data, + pa->padata_value.length, + &enc_data, + &size); + if (ret) { + ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; + _kdc_r_log(r, 4, "Failed to decode PA-DATA -- %s", + r->cname); + return ret; + } + + pepper1client.data = "clientchallengearmor"; + pepper1client.length = strlen(pepper1client.data); + pepper1kdc.data = "kdcchallengearmor"; + pepper1kdc.length = strlen(pepper1kdc.data); + pepper2.data = "challengelongterm"; + pepper2.length = strlen(pepper2.data); + + krb5_crypto_getenctype(r->context, r->armor_crypto, &aenctype); + + kdc_log(r->context, r->config, 5, "FAST armor enctype is: %d", (int)aenctype); + + ret = pa_enc_chal_decrypt_kvno(r, aenctype, + &pepper1client, + &pepper1kdc, + &pepper2, + kvno, + &enc_data, + &KDCchallengekey, + &k); + if (ret == KRB5KDC_ERR_ETYPE_NOSUPP) { + char *estr; + _kdc_set_e_text(r, "No key matching entype"); + if(krb5_enctype_to_string(r->context, enc_data.etype, &estr)) + estr = NULL; + if(estr == NULL) + _kdc_r_log(r, 4, + "No client key matching ENC-CHAL (%d) -- %s", + enc_data.etype, r->cname); + else + _kdc_r_log(r, 4, + "No client key matching ENC-CHAL (%s) -- %s", + estr, r->cname); + free(estr); + free_EncryptedData(&enc_data); + kdc_audit_setkv_number((kdc_request_t)r, + KDC_REQUEST_KV_PA_FAILED_KVNO, + kvno); + return ret; + } + if (ret == KRB5KRB_AP_ERR_SKEW) { + /* + * Logging happens inside of + * _krb5_validate_pa_enc_challenge() + * via pa_enc_chal_decrypt_kvno() + */ + + free_EncryptedData(&enc_data); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_CLIENT_TIME_SKEW); + + /* + * The following is needed to make windows clients to + * retry using the timestamp in the error message, if + * there is a e_text, they become unhappy. + */ + r->e_text = NULL; + return ret; + } + if (ret == KRB5KDC_ERR_PREAUTH_FAILED) { + krb5_error_code hret = ret; + int hi; + + /* + * Logging happens inside of + * via pa_enc_chal_decrypt_kvno() + */ + + kdc_audit_setkv_number((kdc_request_t)r, + KDC_REQUEST_KV_PA_FAILED_KVNO, + kvno); + + /* + * Check if old and older keys are + * able to decrypt. + */ + for (hi = 1; hi < 3; hi++) { + krb5_kvno hkvno; + + if (hi >= kvno) { + break; + } + + hkvno = kvno - hi; + hret = pa_enc_chal_decrypt_kvno(r, aenctype, + &pepper1client, + NULL, /* pepper1kdc */ + &pepper2, + hkvno, + &enc_data, + NULL, /* KDCchallengekey */ + NULL); /* used_key */ + if (hret == 0) { + kdc_audit_setkv_number((kdc_request_t)r, + KDC_REQUEST_KV_PA_HISTORIC_KVNO, + hkvno); + break; + } + if (hret == KRB5KDC_ERR_ETYPE_NOSUPP) { + break; + } + } + + free_EncryptedData(&enc_data); + + if (hret == 0) + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_HISTORIC_LONG_TERM_KEY); + else + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_WRONG_LONG_TERM_KEY); + + return ret; + } + free_EncryptedData(&enc_data); + if (ret == 0) { + krb5_crypto challengecrypto; + char *estr = NULL; + char *astr = NULL; + char *kstr = NULL; + + ret = krb5_crypto_init(r->context, &KDCchallengekey, 0, &challengecrypto); + krb5_free_keyblock_contents(r->context, &KDCchallengekey); + if (ret) + return ret; + + ret = _krb5_make_pa_enc_challenge(r->context, challengecrypto, + KRB5_KU_ENC_CHALLENGE_KDC, + r->rep.padata); + krb5_crypto_destroy(r->context, challengecrypto); + if (ret) + return ret; + + ret = set_salt_padata(r->context, r->config, r->rep.padata, k); + if (ret) + return ret; + + /* + * Found a key that the client used, lets pick that as the reply key + */ + + krb5_free_keyblock_contents(r->context, &r->reply_key); + ret = krb5_copy_keyblock_contents(r->context, &k->key, &r->reply_key); + if (ret) + return ret; + + if (krb5_enctype_to_string(r->context, (int)aenctype, &astr)) + astr = NULL; + if (krb5_enctype_to_string(r->context, enc_data.etype, &estr)) + estr = NULL; + if (krb5_enctype_to_string(r->context, k->key.keytype, &kstr)) + kstr = NULL; + _kdc_r_log(r, 4, "ENC-CHAL Pre-authentication succeeded -- %s " + "using armor=%s enc=%s key=%s", + r->cname, + astr ? astr : "unknown enctype", + estr ? estr : "unknown enctype", + kstr ? kstr : "unknown enctype"); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_VALIDATED_LONG_TERM_KEY); + kdc_audit_setkv_number((kdc_request_t)r, + KDC_REQUEST_KV_PA_SUCCEEDED_KVNO, + kvno); + return 0; + } + + return ret; +} + +static krb5_error_code +pa_enc_ts_decrypt_kvno(astgs_request_t r, + krb5_kvno kvno, + const EncryptedData *enc_data, + krb5_data *ts_data, + Key **_pa_key) +{ + krb5_error_code ret; + krb5_crypto crypto; + Key *pa_key = NULL; + const Keys *keys = NULL; + + if (_pa_key) + *_pa_key = NULL; + + krb5_data_zero(ts_data); + + keys = hdb_kvno2keys(r->context, r->client, kvno); + if (keys == NULL) { + return KRB5KDC_ERR_ETYPE_NOSUPP; + } + ret = hdb_enctype2key(r->context, r->client, keys, + enc_data->etype, &pa_key); + if(ret){ + return KRB5KDC_ERR_ETYPE_NOSUPP; + } + + try_next_key: + ret = krb5_crypto_init(r->context, &pa_key->key, 0, &crypto); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + _kdc_r_log(r, 4, "krb5_crypto_init failed: %s", msg); + krb5_free_error_message(r->context, msg); + return ret; + } + + ret = krb5_decrypt_EncryptedData(r->context, + crypto, + KRB5_KU_PA_ENC_TIMESTAMP, + enc_data, + ts_data); + krb5_crypto_destroy(r->context, crypto); + /* + * Since the user might have several keys with the same + * enctype but with diffrent salting, we need to try all + * the keys with the same enctype. + */ + if (ret) { + ret = hdb_next_enctype2key(r->context, r->client, keys, + enc_data->etype, &pa_key); + if (ret == 0) + goto try_next_key; + + return KRB5KDC_ERR_PREAUTH_FAILED; + } + + if (_pa_key) + *_pa_key = pa_key; + return 0; +} + +static krb5_error_code +pa_enc_ts_validate(astgs_request_t r, const PA_DATA *pa) +{ + krb5_kvno kvno = r->client->kvno; + EncryptedData enc_data; + krb5_error_code ret; + krb5_data ts_data; + PA_ENC_TS_ENC p; + size_t len; + Key *pa_key; + char *str; + + if (r->armor_crypto && !r->config->enable_armored_pa_enc_timestamp) { + ret = KRB5KDC_ERR_POLICY; + kdc_log(r->context, r->config, 0, + "Armored encrypted timestamp pre-authentication is disabled"); + return ret; + } else if (!r->armor_crypto && !r->config->enable_unarmored_pa_enc_timestamp) { + ret = KRB5KDC_ERR_POLICY; + kdc_log(r->context, r->config, 0, + "Unarmored encrypted timestamp pre-authentication is disabled"); + return ret; + } + + if (r->client->flags.locked_out) { + ret = KRB5KDC_ERR_CLIENT_REVOKED; + kdc_log(r->context, r->config, 0, + "Client (%s) is locked out", r->cname); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_CLIENT_LOCKED_OUT); + return ret; + } + + ret = decode_EncryptedData(pa->padata_value.data, + pa->padata_value.length, + &enc_data, + &len); + if (ret) { + ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; + _kdc_r_log(r, 4, "Failed to decode PA-DATA -- %s", + r->cname); + goto out; + } + + ret = pa_enc_ts_decrypt_kvno(r, kvno, &enc_data, &ts_data, &pa_key); + if (ret == KRB5KDC_ERR_ETYPE_NOSUPP) { + char *estr; + _kdc_set_e_text(r, "No key matching enctype"); + if(krb5_enctype_to_string(r->context, enc_data.etype, &estr)) + estr = NULL; + if(estr == NULL) + _kdc_r_log(r, 4, + "No client key matching pa-data (%d) -- %s", + enc_data.etype, r->cname); + else + _kdc_r_log(r, 4, + "No client key matching pa-data (%s) -- %s", + estr, r->cname); + free(estr); + free_EncryptedData(&enc_data); + kdc_audit_setkv_number((kdc_request_t)r, + KDC_REQUEST_KV_PA_FAILED_KVNO, + kvno); + goto out; + } + + if (ret == KRB5KDC_ERR_PREAUTH_FAILED) { + krb5_error_code ret2; + const char *msg = krb5_get_error_message(r->context, ret); + krb5_error_code hret = ret; + int hi; + + kdc_audit_setkv_number((kdc_request_t)r, + KDC_REQUEST_KV_PA_FAILED_KVNO, + kvno); + + /* + * Check if old and older keys are + * able to decrypt. + */ + for (hi = 1; hi < 3; hi++) { + krb5_kvno hkvno; + + if (hi >= kvno) { + break; + } + + hkvno = kvno - hi; + hret = pa_enc_ts_decrypt_kvno(r, hkvno, + &enc_data, + &ts_data, + NULL); /* pa_key */ + if (hret == 0) { + krb5_data_free(&ts_data); + kdc_audit_setkv_number((kdc_request_t)r, + KDC_REQUEST_KV_PA_HISTORIC_KVNO, + hkvno); + break; + } + if (hret == KRB5KDC_ERR_ETYPE_NOSUPP) { + break; + } + } + + ret2 = krb5_enctype_to_string(r->context, enc_data.etype, &str); + if (ret2) + str = NULL; + _kdc_r_log(r, 2, "Failed to decrypt PA-DATA -- %s " + "(enctype %s) error %s", + r->cname, str ? str : "unknown enctype", msg); + krb5_xfree(str); + krb5_free_error_message(r->context, msg); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_PA_ETYPE, + enc_data.etype); + if (hret == 0) + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_HISTORIC_LONG_TERM_KEY); + else + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_WRONG_LONG_TERM_KEY); + + free_EncryptedData(&enc_data); + + ret = KRB5KDC_ERR_PREAUTH_FAILED; + goto out; + } + free_EncryptedData(&enc_data); + ret = decode_PA_ENC_TS_ENC(ts_data.data, + ts_data.length, + &p, + &len); + krb5_data_free(&ts_data); + if(ret){ + ret = KRB5KDC_ERR_PREAUTH_FAILED; + _kdc_r_log(r, 4, "Failed to decode PA-ENC-TS-ENC -- %s", + r->cname); + goto out; + } + if (labs(kdc_time - p.patimestamp) > r->context->max_skew) { + char client_time[100]; + + krb5_format_time(r->context, p.patimestamp, + client_time, sizeof(client_time), TRUE); + + ret = KRB5KRB_AP_ERR_SKEW; + _kdc_r_log(r, 4, "Too large time skew, " + "client time %s is out by %u > %u seconds -- %s", + client_time, + (unsigned)labs(kdc_time - p.patimestamp), + r->context->max_skew, + r->cname); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_CLIENT_TIME_SKEW); + + /* + * The following is needed to make windows clients to + * retry using the timestamp in the error message, if + * there is a e_text, they become unhappy. + */ + r->e_text = NULL; + free_PA_ENC_TS_ENC(&p); + goto out; + } + free_PA_ENC_TS_ENC(&p); + + ret = set_salt_padata(r->context, r->config, r->rep.padata, pa_key); + if (ret == 0) + ret = krb5_copy_keyblock_contents(r->context, &pa_key->key, &r->reply_key); + if (ret) + return ret; + + ret = krb5_enctype_to_string(r->context, pa_key->key.keytype, &str); + if (ret) + str = NULL; + _kdc_r_log(r, 4, "ENC-TS Pre-authentication succeeded -- %s using %s", + r->cname, str ? str : "unknown enctype"); + krb5_xfree(str); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_PA_ETYPE, + pa_key->key.keytype); + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_VALIDATED_LONG_TERM_KEY); + kdc_audit_setkv_number((kdc_request_t)r, + KDC_REQUEST_KV_PA_SUCCEEDED_KVNO, + kvno); + + ret = 0; + + out: + + return ret; +} + +#ifdef PKINIT + +static krb5_error_code +make_freshness_token(astgs_request_t r, const Key *krbtgt_key, unsigned krbtgt_kvno) +{ + krb5_error_code ret = 0; + const struct timeval current_kdc_time = krb5_kdc_get_time(); + int usec = current_kdc_time.tv_usec; + const PA_ENC_TS_ENC ts_enc = { + .patimestamp = current_kdc_time.tv_sec, + .pausec = &usec, + }; + unsigned char *encoded_ts_enc = NULL; + size_t ts_enc_size; + size_t ts_enc_len = 0; + EncryptedData encdata; + krb5_crypto crypto; + unsigned char *token = NULL; + size_t token_size; + size_t token_len = 0; + size_t token_alloc_size; + + ASN1_MALLOC_ENCODE(PA_ENC_TS_ENC, + encoded_ts_enc, + ts_enc_size, + &ts_enc, + &ts_enc_len, + ret); + if (ret) + return ret; + if (ts_enc_size != ts_enc_len) + krb5_abortx(r->context, "internal error in ASN.1 encoder"); + + ret = krb5_crypto_init(r->context, &krbtgt_key->key, 0, &crypto); + if (ret) { + free(encoded_ts_enc); + return ret; + } + + ret = krb5_encrypt_EncryptedData(r->context, + crypto, + KRB5_KU_AS_FRESHNESS, + encoded_ts_enc, + ts_enc_len, + krbtgt_kvno, + &encdata); + free(encoded_ts_enc); + krb5_crypto_destroy(r->context, crypto); + if (ret) + return ret; + + token_size = length_EncryptedData(&encdata); + token_alloc_size = token_size + 2; /* Account for the two leading zero bytes. */ + token = calloc(1, token_alloc_size); + if (token == NULL) { + free_EncryptedData(&encdata); + return ENOMEM; + } + + ret = encode_EncryptedData(token + token_alloc_size - 1, + token_size, + &encdata, + &token_len); + free_EncryptedData(&encdata); + if (ret) { + free(token); + return ret; + } + if (token_size != token_len) + krb5_abortx(r->context, "internal error in ASN.1 encoder"); + + ret = krb5_padata_add(r->context, + r->rep.padata, + KRB5_PADATA_AS_FRESHNESS, + token, + token_alloc_size); + if (ret) + free(token); + return ret; +} + +#endif /* PKINIT */ + +static krb5_error_code +send_freshness_token(astgs_request_t r, const Key *krbtgt_key, unsigned krbtgt_kvno) +{ + krb5_error_code ret = 0; +#ifdef PKINIT + int idx = 0; + const PA_DATA *freshness_padata = NULL; + + freshness_padata = _kdc_find_padata(&r->req, + &idx, + KRB5_PADATA_AS_FRESHNESS); + if (freshness_padata == NULL) { + return 0; + } + + ret = make_freshness_token(r, krbtgt_key, krbtgt_kvno); +#endif /* PKINIT */ + return ret; +} + +struct kdc_patypes { + int type; + const char *name; + unsigned int flags; +#define PA_ANNOUNCE 1 +#define PA_REQ_FAST 2 /* only use inside fast */ +#define PA_SYNTHETIC_OK 4 +#define PA_REPLACE_REPLY_KEY 8 /* PA mech replaces reply key */ +#define PA_USES_LONG_TERM_KEY 16 /* PA mech uses client's long-term key */ +#define PA_USES_FAST_COOKIE 32 /* Multi-step PA mech maintains state in PA-FX-COOKIE */ + krb5_error_code (*validate)(astgs_request_t, const PA_DATA *pa); + krb5_error_code (*finalize_pac)(astgs_request_t r); + void (*cleanup)(astgs_request_t r); +}; + +static const struct kdc_patypes pat[] = { +#ifdef PKINIT + { + KRB5_PADATA_PK_AS_REQ, "PK-INIT(ietf)", + PA_ANNOUNCE | PA_SYNTHETIC_OK | PA_REPLACE_REPLY_KEY, + pa_pkinit_validate, NULL, NULL + }, + { + KRB5_PADATA_PK_AS_REQ_WIN, "PK-INIT(win2k)", PA_ANNOUNCE | PA_REPLACE_REPLY_KEY, + pa_pkinit_validate, NULL, NULL + }, + { + KRB5_PADATA_PKINIT_KX, "Anonymous PK-INIT", PA_ANNOUNCE, + NULL, NULL, NULL + }, +#else + { KRB5_PADATA_PK_AS_REQ, "PK-INIT(ietf)", 0, NULL , NULL, NULL }, + { KRB5_PADATA_PK_AS_REQ_WIN, "PK-INIT(win2k)", 0, NULL, NULL, NULL }, + { KRB5_PADATA_PKINIT_KX, "Anonymous PK-INIT", 0, NULL, NULL, NULL }, +#endif + { KRB5_PADATA_PA_PK_OCSP_RESPONSE , "OCSP", 0, NULL, NULL, NULL }, + { + KRB5_PADATA_ENC_TIMESTAMP , "ENC-TS", + PA_ANNOUNCE | PA_USES_LONG_TERM_KEY, + pa_enc_ts_validate, NULL, NULL + }, + { + KRB5_PADATA_ENCRYPTED_CHALLENGE , "ENC-CHAL", + PA_ANNOUNCE | PA_USES_LONG_TERM_KEY | PA_REQ_FAST, + pa_enc_chal_validate, NULL, NULL + }, + { KRB5_PADATA_REQ_ENC_PA_REP , "REQ-ENC-PA-REP", 0, NULL, NULL, NULL }, + { KRB5_PADATA_FX_FAST, "FX-FAST", PA_ANNOUNCE, NULL, NULL, NULL }, + { KRB5_PADATA_FX_ERROR, "FX-ERROR", 0, NULL, NULL, NULL }, + { KRB5_PADATA_FX_COOKIE, "FX-COOKIE", 0, NULL, NULL, NULL }, + { + KRB5_PADATA_GSS , "GSS", + PA_ANNOUNCE | PA_SYNTHETIC_OK | PA_REPLACE_REPLY_KEY | PA_USES_FAST_COOKIE, + pa_gss_validate, pa_gss_finalize_pac, NULL + }, +}; + +static void +log_patypes(astgs_request_t r, METHOD_DATA *padata) +{ + krb5_kdc_configuration *config = r->config; + struct rk_strpool *p = NULL; + char *str; + size_t n, m; + + for (n = 0; n < padata->len; n++) { + for (m = 0; m < sizeof(pat) / sizeof(pat[0]); m++) { + if (padata->val[n].padata_type == pat[m].type) { + p = rk_strpoolprintf(p, "%s", pat[m].name); + break; + } + } + if (m == sizeof(pat) / sizeof(pat[0])) + p = rk_strpoolprintf(p, "%d", padata->val[n].padata_type); + if (p && n + 1 < padata->len) + p = rk_strpoolprintf(p, ", "); + if (p == NULL) { + kdc_log(r->context, config, 1, "out of memory"); + return; + } + } + if (p == NULL) + p = rk_strpoolprintf(p, "none"); + + str = rk_strpoolcollect(p); + kdc_log(r->context, config, 4, "Client sent patypes: %s", str); + kdc_audit_addkv((kdc_request_t)r, KDC_AUDIT_EATWHITE, + "client-pa", "%s", str); + free(str); +} + +static krb5_boolean +pa_used_flag_isset(astgs_request_t r, unsigned int flag) +{ + if (r->pa_used == NULL) + return FALSE; + + return (r->pa_used->flags & flag) == flag; +} + +/* + * + */ + +krb5_error_code +_kdc_encode_reply(krb5_context context, + krb5_kdc_configuration *config, + astgs_request_t r, uint32_t nonce, + krb5_enctype etype, + int skvno, const EncryptionKey *skey, + int ckvno, + int rk_is_subkey, + krb5_data *reply) +{ + unsigned char *buf; + size_t buf_size; + size_t len = 0; + krb5_error_code ret; + krb5_crypto crypto; + KDC_REP *rep = &r->rep; + EncTicketPart *et = &r->et; + EncKDCRepPart *ek = &r->ek; + + heim_assert(rep->padata != NULL, "reply padata uninitialized"); + + ASN1_MALLOC_ENCODE(EncTicketPart, buf, buf_size, et, &len, ret); + if(ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 4, "Failed to encode ticket: %s", msg); + krb5_free_error_message(context, msg); + return ret; + } + if(buf_size != len) + krb5_abortx(context, "Internal error in ASN.1 encoder"); + + ret = krb5_crypto_init(context, skey, etype, &crypto); + if (ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 4, "krb5_crypto_init failed: %s", msg); + krb5_free_error_message(context, msg); + free(buf); + return ret; + } + + ret = krb5_encrypt_EncryptedData(context, + crypto, + KRB5_KU_TICKET, + buf, + len, + skvno, + &rep->ticket.enc_part); + free(buf); + krb5_crypto_destroy(context, crypto); + if(ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 4, "Failed to encrypt data: %s", msg); + krb5_free_error_message(context, msg); + return ret; + } + + if (r && r->armor_crypto) { + KrbFastFinished finished; + krb5_data data; + + kdc_log(context, config, 4, "FAST armor protection"); + + memset(&finished, 0, sizeof(finished)); + krb5_data_zero(&data); + + finished.timestamp = kdc_time; + finished.usec = 0; + finished.crealm = et->crealm; + finished.cname = et->cname; + + ASN1_MALLOC_ENCODE(Ticket, data.data, data.length, + &rep->ticket, &len, ret); + if (ret) + return ret; + if (data.length != len) + krb5_abortx(context, "internal asn.1 error"); + + ret = krb5_create_checksum(context, r->armor_crypto, + KRB5_KU_FAST_FINISHED, 0, + data.data, data.length, + &finished.ticket_checksum); + krb5_data_free(&data); + if (ret) + return ret; + + ret = _kdc_fast_mk_response(context, r->armor_crypto, + rep->padata, &r->strengthen_key, &finished, + nonce, &data); + free_Checksum(&finished.ticket_checksum); + if (ret) + return ret; + + free_METHOD_DATA(r->rep.padata); + + ret = krb5_padata_add(context, rep->padata, + KRB5_PADATA_FX_FAST, + data.data, data.length); + if (ret) + return ret; + + /* + * Hide client name for privacy reasons + */ + if (r->fast.flags.requested_hidden_names) { + Realm anon_realm = KRB5_ANON_REALM; + + free_Realm(&rep->crealm); + ret = copy_Realm(&anon_realm, &rep->crealm); + if (ret == 0) { + free_PrincipalName(&rep->cname); + ret = _kdc_make_anonymous_principalname(&rep->cname); + } + if (ret) + return ret; + } + } + + if (rep->padata->len == 0) { + free_METHOD_DATA(rep->padata); + free(rep->padata); + rep->padata = NULL; + } + + if(rep->msg_type == krb_as_rep && !config->encode_as_rep_as_tgs_rep) + ASN1_MALLOC_ENCODE(EncASRepPart, buf, buf_size, ek, &len, ret); + else + ASN1_MALLOC_ENCODE(EncTGSRepPart, buf, buf_size, ek, &len, ret); + if(ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 4, "Failed to encode KDC-REP: %s", msg); + krb5_free_error_message(context, msg); + return ret; + } + if(buf_size != len) { + free(buf); + kdc_log(context, config, 4, "Internal error in ASN.1 encoder"); + _kdc_set_e_text(r, "KDC internal error"); + return KRB5KRB_ERR_GENERIC; + } + ret = krb5_crypto_init(context, &r->reply_key, 0, &crypto); + if (ret) { + const char *msg = krb5_get_error_message(context, ret); + free(buf); + kdc_log(context, config, 4, "krb5_crypto_init failed: %s", msg); + krb5_free_error_message(context, msg); + return ret; + } + if(rep->msg_type == krb_as_rep) { + ret = krb5_encrypt_EncryptedData(context, + crypto, + KRB5_KU_AS_REP_ENC_PART, + buf, + len, + ckvno, + &rep->enc_part); + free(buf); + if (ret == 0) + ASN1_MALLOC_ENCODE(AS_REP, buf, buf_size, rep, &len, ret); + } else { + ret = krb5_encrypt_EncryptedData(context, + crypto, + rk_is_subkey ? + KRB5_KU_TGS_REP_ENC_PART_SUB_KEY : + KRB5_KU_TGS_REP_ENC_PART_SESSION, + buf, + len, + ckvno, + &rep->enc_part); + free(buf); + if (ret == 0) + ASN1_MALLOC_ENCODE(TGS_REP, buf, buf_size, rep, &len, ret); + } + krb5_crypto_destroy(context, crypto); + if(ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 4, "Failed to encode KDC-REP: %s", msg); + krb5_free_error_message(context, msg); + return ret; + } + if(buf_size != len) { + free(buf); + kdc_log(context, config, 4, "Internal error in ASN.1 encoder"); + _kdc_set_e_text(r, "KDC internal error"); + return KRB5KRB_ERR_GENERIC; + } + reply->data = buf; + reply->length = buf_size; + return 0; +} + +/* + * + */ + +static krb5_error_code +get_pa_etype_info(krb5_context context, + krb5_kdc_configuration *config, + METHOD_DATA *md, Key *ckey, + krb5_boolean include_salt) +{ + krb5_error_code ret = 0; + ETYPE_INFO_ENTRY eie; /* do not free this one */ + ETYPE_INFO ei; + PA_DATA pa; + size_t len; + + /* + * Code moved here from what used to be make_etype_info_entry() because + * using the ASN.1 compiler-generated SEQUENCE OF add functions makes that + * old function's body and this one's small and clean. + * + * The following comment blocks were there: + * + * According to `the specs', we can't send a salt if we have AFS3 salted + * key, but that requires that you *know* what cell you are using (e.g by + * assuming that the cell is the same as the realm in lower case) + * + * We shouldn't sent salttype since it is incompatible with the + * specification and it breaks windows clients. The afs salting problem + * is solved by using KRB5-PADATA-AFS3-SALT implemented in Heimdal 0.7 and + * later. + * + * We return no salt type at all, as that should indicate the default salt + * type and make everybody happy. some systems (like w2k) dislike being + * told the salt type here. + */ + + pa.padata_type = KRB5_PADATA_ETYPE_INFO; + pa.padata_value.data = NULL; + pa.padata_value.length = 0; + ei.len = 0; + ei.val = NULL; + eie.etype = ckey->key.keytype; + eie.salttype = NULL; + eie.salt = NULL; + if (include_salt && ckey->salt) + eie.salt = &ckey->salt->salt; + ret = add_ETYPE_INFO(&ei, &eie); + if (ret == 0) + ASN1_MALLOC_ENCODE(ETYPE_INFO, pa.padata_value.data, pa.padata_value.length, + &ei, &len, ret); + if (ret == 0) + add_METHOD_DATA(md, &pa); + free_ETYPE_INFO(&ei); + free_PA_DATA(&pa); + return ret; +} + +/* + * + */ + +extern const int _krb5_AES_SHA1_string_to_default_iterator; +extern const int _krb5_AES_SHA2_string_to_default_iterator; + +static krb5_error_code +make_s2kparams(int value, size_t len, krb5_data **ps2kparams) +{ + krb5_data *s2kparams; + krb5_error_code ret; + + ALLOC(s2kparams); + if (s2kparams == NULL) + return ENOMEM; + ret = krb5_data_alloc(s2kparams, len); + if (ret) { + free(s2kparams); + return ret; + } + _krb5_put_int(s2kparams->data, value, len); + *ps2kparams = s2kparams; + return 0; +} + +static krb5_error_code +make_etype_info2_entry(ETYPE_INFO2_ENTRY *ent, + Key *key, + krb5_boolean include_salt) +{ + krb5_error_code ret; + + ent->etype = key->key.keytype; + if (key->salt && include_salt) { + ALLOC(ent->salt); + if (ent->salt == NULL) + return ENOMEM; + *ent->salt = malloc(key->salt->salt.length + 1); + if (*ent->salt == NULL) { + free(ent->salt); + ent->salt = NULL; + return ENOMEM; + } + memcpy(*ent->salt, key->salt->salt.data, key->salt->salt.length); + (*ent->salt)[key->salt->salt.length] = '\0'; + } else + ent->salt = NULL; + + ent->s2kparams = NULL; + + switch (key->key.keytype) { + case ETYPE_AES128_CTS_HMAC_SHA1_96: + case ETYPE_AES256_CTS_HMAC_SHA1_96: + ret = make_s2kparams(_krb5_AES_SHA1_string_to_default_iterator, + 4, &ent->s2kparams); + break; + case KRB5_ENCTYPE_AES128_CTS_HMAC_SHA256_128: + case KRB5_ENCTYPE_AES256_CTS_HMAC_SHA384_192: + ret = make_s2kparams(_krb5_AES_SHA2_string_to_default_iterator, + 4, &ent->s2kparams); + break; + case ETYPE_DES_CBC_CRC: + case ETYPE_DES_CBC_MD4: + case ETYPE_DES_CBC_MD5: + /* Check if this was a AFS3 salted key */ + if(key->salt && key->salt->type == hdb_afs3_salt) + ret = make_s2kparams(1, 1, &ent->s2kparams); + else + ret = 0; + break; + default: + ret = 0; + break; + } + return ret; +} + +/* + * Return an ETYPE-INFO2. Enctypes are storted the same way as in the + * database (client supported enctypes first, then the unsupported + * enctypes). + */ + +static krb5_error_code +get_pa_etype_info2(krb5_context context, + krb5_kdc_configuration *config, + METHOD_DATA *md, Key *ckey, + krb5_boolean include_salt) +{ + krb5_error_code ret = 0; + ETYPE_INFO2 pa; + unsigned char *buf; + size_t len; + + pa.len = 1; + pa.val = calloc(1, sizeof(pa.val[0])); + if(pa.val == NULL) + return ENOMEM; + + ret = make_etype_info2_entry(&pa.val[0], ckey, include_salt); + if (ret) { + free_ETYPE_INFO2(&pa); + return ret; + } + + ASN1_MALLOC_ENCODE(ETYPE_INFO2, buf, len, &pa, &len, ret); + free_ETYPE_INFO2(&pa); + if(ret) + return ret; + ret = realloc_method_data(md); + if(ret) { + free(buf); + return ret; + } + md->val[md->len - 1].padata_type = KRB5_PADATA_ETYPE_INFO2; + md->val[md->len - 1].padata_value.length = len; + md->val[md->len - 1].padata_value.data = buf; + return 0; +} + +/* + * Return 0 if the client has only older enctypes, this is for + * determining if the server should send ETYPE_INFO2 or not. + */ + +static int +newer_enctype_present(krb5_context context, + struct KDC_REQ_BODY_etype *etype_list) +{ + size_t i; + + for (i = 0; i < etype_list->len; i++) { + if (!krb5_is_enctype_old(context, etype_list->val[i])) + return 1; + } + return 0; +} + +static krb5_error_code +get_pa_etype_info_both(krb5_context context, + krb5_kdc_configuration *config, + struct KDC_REQ_BODY_etype *etype_list, + METHOD_DATA *md, Key *ckey, + krb5_boolean include_salt) +{ + krb5_error_code ret; + + /* + * Windows 2019 (and earlier versions) always sends the salt + * and Samba has testsuites that check this behaviour, so a + * Samba AD DC will set this flag to match the AS-REP packet + * more closely. + */ + if (config->force_include_pa_etype_salt) + include_salt = TRUE; + + /* + * RFC4120 requires: + * When the AS server is to include pre-authentication data in a + * KRB-ERROR or in an AS-REP, it MUST use PA-ETYPE-INFO2, not + * PA-ETYPE-INFO, if the etype field of the client's AS-REQ lists + * at least one "newer" encryption type. Otherwise (when the etype + * field of the client's AS-REQ does not list any "newer" encryption + * types), it MUST send both PA-ETYPE-INFO2 and PA-ETYPE-INFO (both + * with an entry for each enctype). A "newer" enctype is any enctype + * first officially specified concurrently with or subsequent to the + * issue of this RFC. The enctypes DES, 3DES, or RC4 and any defined + * in [RFC1510] are not "newer" enctypes. + * + * It goes on to state: + * The preferred ordering of the "hint" pre-authentication data that + * affect client key selection is: ETYPE-INFO2, followed by ETYPE-INFO, + * followed by PW-SALT. As noted in Section 3.1.3, a KDC MUST NOT send + * ETYPE-INFO or PW-SALT when the client's AS-REQ includes at least one + * "newer" etype. + */ + + ret = get_pa_etype_info2(context, config, md, ckey, include_salt); + if (ret) + return ret; + + if (!newer_enctype_present(context, etype_list)) + ret = get_pa_etype_info(context, config, md, ckey, include_salt); + + return ret; +} + +/* + * + */ + +void +_log_astgs_req(astgs_request_t r, krb5_enctype setype) +{ + const KDC_REQ_BODY *b = &r->req.req_body; + krb5_enctype cetype = r->reply_key.keytype; + krb5_error_code ret; + struct rk_strpool *p; + struct rk_strpool *s = NULL; + char *str; + char *cet; + char *set; + size_t i; + + /* + * we are collecting ``p'' and ``s''. The former is a textual + * representation of the enctypes as strings which will be used + * for debugging. The latter is a terse comma separated list of + * the %d's of the enctypes to emit into our audit trail to + * conserve space in the logs. + */ + + p = rk_strpoolprintf(NULL, "%s", "Client supported enctypes: "); + + for (i = 0; i < b->etype.len; i++) { + ret = krb5_enctype_to_string(r->context, b->etype.val[i], &str); + if (ret == 0) { + p = rk_strpoolprintf(p, "%s", str); + free(str); + } else + p = rk_strpoolprintf(p, "%d", b->etype.val[i]); + if (p == NULL) { + rk_strpoolfree(s); + _kdc_r_log(r, 4, "out of memory"); + return; + } + s = rk_strpoolprintf(s, "%d", b->etype.val[i]); + if (i + 1 < b->etype.len) { + p = rk_strpoolprintf(p, ", "); + s = rk_strpoolprintf(s, ","); + } + } + if (p == NULL) + p = rk_strpoolprintf(p, "no encryption types"); + + str = rk_strpoolcollect(s); + if (str) + kdc_audit_addkv((kdc_request_t)r, KDC_AUDIT_EATWHITE, "etypes", "%s", + str); + free(str); + + ret = krb5_enctype_to_string(r->context, cetype, &cet); + if(ret == 0) { + ret = krb5_enctype_to_string(r->context, setype, &set); + if (ret == 0) { + p = rk_strpoolprintf(p, ", using %s/%s", cet, set); + free(set); + } + free(cet); + } + if (ret != 0) + p = rk_strpoolprintf(p, ", using enctypes %d/%d", + cetype, setype); + + str = rk_strpoolcollect(p); + if (str) + _kdc_r_log(r, 4, "%s", str); + free(str); + + kdc_audit_addkv((kdc_request_t)r, 0, "etype", "%d/%d", cetype, setype); + + { + char fixedstr[128]; + int result; + + result = unparse_flags(KDCOptions2int(b->kdc_options), asn1_KDCOptions_units(), + fixedstr, sizeof(fixedstr)); + if (result > 0) { + _kdc_r_log(r, 4, "Requested flags: %s", fixedstr); + kdc_audit_addkv((kdc_request_t)r, KDC_AUDIT_EATWHITE, + "flags", "%s", fixedstr); + } + } +} + +/* + * verify the flags on `client' and `server', returning 0 + * if they are OK and generating an error messages and returning + * and error code otherwise. + */ + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +kdc_check_flags(astgs_request_t r, + krb5_boolean is_as_req, + hdb_entry *client, + hdb_entry *server) +{ + if (client != NULL) { + /* check client */ + if (client->flags.locked_out) { + kdc_audit_addreason((kdc_request_t)r, "Client is locked out"); + return KRB5KDC_ERR_CLIENT_REVOKED; + } + + if (client->flags.invalid) { + kdc_audit_addreason((kdc_request_t)r, + "Client has invalid bit set"); + return KRB5KDC_ERR_POLICY; + } + + if (!client->flags.client) { + kdc_audit_addreason((kdc_request_t)r, + "Principal may not act as client"); + return KRB5KDC_ERR_POLICY; + } + + if (client->valid_start && *client->valid_start > kdc_time) { + char starttime_str[100]; + krb5_format_time(r->context, *client->valid_start, + starttime_str, sizeof(starttime_str), TRUE); + kdc_audit_addreason((kdc_request_t)r, "Client not yet valid " + "until %s", starttime_str); + return KRB5KDC_ERR_CLIENT_NOTYET; + } + + if (client->valid_end && *client->valid_end < kdc_time) { + char endtime_str[100]; + krb5_format_time(r->context, *client->valid_end, + endtime_str, sizeof(endtime_str), TRUE); + kdc_audit_addreason((kdc_request_t)r, "Client expired at %s", + endtime_str); + return KRB5KDC_ERR_NAME_EXP; + } + + if (client->flags.require_pwchange && + (server == NULL || !server->flags.change_pw)) + return KRB5KDC_ERR_KEY_EXPIRED; + + if (client->pw_end && *client->pw_end < kdc_time + && (server == NULL || !server->flags.change_pw)) { + char pwend_str[100]; + krb5_format_time(r->context, *client->pw_end, + pwend_str, sizeof(pwend_str), TRUE); + kdc_audit_addreason((kdc_request_t)r, "Client's key has expired " + "at %s", pwend_str); + return KRB5KDC_ERR_KEY_EXPIRED; + } + } + + /* check server */ + + if (server != NULL) { + if (server->flags.locked_out) { + kdc_audit_addreason((kdc_request_t)r, "Server locked out"); + return KRB5KDC_ERR_SERVICE_REVOKED; + } + if (server->flags.invalid) { + kdc_audit_addreason((kdc_request_t)r, + "Server has invalid flag set"); + return KRB5KDC_ERR_POLICY; + } + if (!server->flags.server) { + kdc_audit_addreason((kdc_request_t)r, + "Principal may not act as server"); + return KRB5KDC_ERR_POLICY; + } + + if (!is_as_req && server->flags.initial) { + kdc_audit_addreason((kdc_request_t)r, + "AS-REQ is required for server"); + return KRB5KDC_ERR_POLICY; + } + + if (server->valid_start && *server->valid_start > kdc_time) { + char starttime_str[100]; + krb5_format_time(r->context, *server->valid_start, + starttime_str, sizeof(starttime_str), TRUE); + kdc_audit_addreason((kdc_request_t)r, "Server not yet valid " + "until %s", starttime_str); + return KRB5KDC_ERR_SERVICE_NOTYET; + } + + if (server->valid_end && *server->valid_end < kdc_time) { + char endtime_str[100]; + krb5_format_time(r->context, *server->valid_end, + endtime_str, sizeof(endtime_str), TRUE); + kdc_audit_addreason((kdc_request_t)r, "Server expired at %s", + endtime_str); + return KRB5KDC_ERR_SERVICE_EXP; + } + + if (server->pw_end && *server->pw_end < kdc_time) { + char pwend_str[100]; + krb5_format_time(r->context, *server->pw_end, + pwend_str, sizeof(pwend_str), TRUE); + kdc_audit_addreason((kdc_request_t)r, "Server's key has expired " + "at %s", pwend_str); + return KRB5KDC_ERR_KEY_EXPIRED; + } + } + return 0; +} + +/* + * Return TRUE if `from' is part of `addresses' taking into consideration + * the configuration variables that tells us how strict we should be about + * these checks + */ + +krb5_boolean +_kdc_check_addresses(astgs_request_t r, HostAddresses *addresses, + const struct sockaddr *from) +{ + krb5_kdc_configuration *config = r->config; + krb5_error_code ret; + krb5_address addr; + krb5_boolean result; + krb5_boolean only_netbios = TRUE; + size_t i; + + if (!config->check_ticket_addresses && !config->warn_ticket_addresses) + return TRUE; + + /* + * Fields of HostAddresses type are always OPTIONAL and should be non- + * empty, but we check for empty just in case as our compiler doesn't + * support size constraints on SEQUENCE OF. + */ + if (addresses == NULL || addresses->len == 0) + return config->allow_null_ticket_addresses; + + for (i = 0; i < addresses->len; ++i) { + if (addresses->val[i].addr_type != KRB5_ADDRESS_NETBIOS) { + only_netbios = FALSE; + } + } + + /* Windows sends it's netbios name, which I can only assume is + * used for the 'allowed workstations' check. This is painful, + * but we still want to check IP addresses if they happen to be + * present. + */ + + if(only_netbios) + return config->allow_null_ticket_addresses; + + ret = krb5_sockaddr2address (r->context, from, &addr); + if(ret) + return FALSE; + + result = krb5_address_search(r->context, &addr, addresses); + krb5_free_address (r->context, &addr); + return result; +} + +/* + * + */ +krb5_error_code +_kdc_check_anon_policy(astgs_request_t r) +{ + if (!r->config->allow_anonymous) { + kdc_audit_addreason((kdc_request_t)r, + "Anonymous tickets denied by local policy"); + return KRB5KDC_ERR_POLICY; + } + + return 0; +} + +/* + * Determine whether the client requested a PAC be included + * or excluded explictly, or whether it doesn't care. + */ + +static uint64_t +get_pac_attributes(krb5_context context, KDC_REQ *req) +{ + krb5_error_code ret; + PA_PAC_REQUEST pacreq; + const PA_DATA *pa; + int i = 0; + uint32_t pac_attributes; + + pa = _kdc_find_padata(req, &i, KRB5_PADATA_PA_PAC_REQUEST); + if (pa == NULL) + return KRB5_PAC_WAS_GIVEN_IMPLICITLY; + + ret = decode_PA_PAC_REQUEST(pa->padata_value.data, + pa->padata_value.length, + &pacreq, + NULL); + if (ret) + return KRB5_PAC_WAS_GIVEN_IMPLICITLY; + + pac_attributes = pacreq.include_pac ? KRB5_PAC_WAS_REQUESTED : 0; + free_PA_PAC_REQUEST(&pacreq); + return pac_attributes; +} + +/* + * + */ + +static krb5_error_code +generate_pac(astgs_request_t r, const Key *skey, const Key *tkey, + krb5_boolean is_tgs) +{ + krb5_error_code ret; + krb5_data data; + uint16_t rodc_id; + krb5_principal client; + krb5_const_principal canon_princ = NULL; + + r->pac_attributes = get_pac_attributes(r->context, &r->req); + kdc_audit_setkv_number((kdc_request_t)r, "pac_attributes", + r->pac_attributes); + + if (!is_tgs && !(r->pac_attributes & (KRB5_PAC_WAS_REQUESTED | KRB5_PAC_WAS_GIVEN_IMPLICITLY))) + return 0; + + /* + * When a PA mech does not use the client's long-term key, the PAC + * may include the client's long-term key (encrypted in the reply key) + * for use by other shared secret authentication protocols, e.g. NTLM. + * Validate a PA mech was actually used before doing this. + */ + + ret = _kdc_pac_generate(r, + r->client, + r->server, + r->pa_used && !pa_used_flag_isset(r, PA_USES_LONG_TERM_KEY) + ? &r->reply_key : NULL, + r->pac_attributes, + &r->pac); + if (ret) { + _kdc_r_log(r, 4, "PAC generation failed for -- %s", + r->cname); + return ret; + } + if (r->pac == NULL) + return 0; + + rodc_id = r->server->kvno >> 16; + + /* libkrb5 expects ticket and PAC client names to match */ + ret = _krb5_principalname2krb5_principal(r->context, &client, + r->et.cname, r->et.crealm); + if (ret) + return ret; + + /* + * Include the canonical name of the principal in the authorization + * data, if the realms match (if they don't, then the KDC could + * impersonate any realm. Windows always canonicalizes the realm, + * but Heimdal permits aliases between realms.) + */ + if (krb5_realm_compare(r->context, client, r->canon_client_princ)) { + char *cpn = NULL; + + canon_princ = r->canon_client_princ; + + (void) krb5_unparse_name(r->context, canon_princ, &cpn); + kdc_audit_addkv((kdc_request_t)r, 0, "canon_client_name", "%s", + cpn ? cpn : "<unknown>"); + krb5_xfree(cpn); + } + + if (r->pa_used && r->pa_used->finalize_pac) { + ret = r->pa_used->finalize_pac(r); + if (ret) + return ret; + } + + ret = _krb5_pac_sign(r->context, + r->pac, + r->et.authtime, + client, + &skey->key, /* Server key */ + &tkey->key, /* TGS key */ + rodc_id, + NULL, /* UPN */ + canon_princ, + FALSE, /* add_full_sig */ + is_tgs ? &r->pac_attributes : NULL, + &data); + krb5_free_principal(r->context, client); + krb5_pac_free(r->context, r->pac); + r->pac = NULL; + if (ret) { + _kdc_r_log(r, 4, "PAC signing failed for -- %s", + r->cname); + return ret; + } + + ret = _kdc_tkt_insert_pac(r->context, &r->et, &data); + krb5_data_free(&data); + + return ret; +} + +/* + * + */ + +krb5_boolean +_kdc_is_anonymous(krb5_context context, krb5_const_principal principal) +{ + return krb5_principal_is_anonymous(context, principal, KRB5_ANON_MATCH_ANY); +} + +/* + * Returns TRUE if principal is the unauthenticated anonymous identity, + * i.e. WELLKNOWN/ANONYMOUS@WELLKNOWN:ANONYMOUS. Unfortunately due to + * backwards compatibility logic in krb5_principal_is_anonymous() we + * have to use our own implementation. + */ + +krb5_boolean +_kdc_is_anonymous_pkinit(krb5_context context, krb5_const_principal principal) +{ + return _kdc_is_anonymous(context, principal) && + strcmp(principal->realm, KRB5_ANON_REALM) == 0; +} + +static int +require_preauth_p(astgs_request_t r) +{ + return r->config->require_preauth + || r->client->flags.require_preauth + || r->server->flags.require_preauth; +} + + +/* + * + */ + +static krb5_error_code +add_enc_pa_rep(astgs_request_t r) +{ + krb5_error_code ret; + krb5_crypto crypto; + Checksum checksum; + krb5_data cdata; + size_t len; + + ret = krb5_crypto_init(r->context, &r->reply_key, 0, &crypto); + if (ret) + return ret; + + ret = krb5_create_checksum(r->context, crypto, + KRB5_KU_AS_REQ, 0, + r->request.data, r->request.length, + &checksum); + krb5_crypto_destroy(r->context, crypto); + if (ret) + return ret; + + ASN1_MALLOC_ENCODE(Checksum, cdata.data, cdata.length, + &checksum, &len, ret); + free_Checksum(&checksum); + if (ret) + return ret; + heim_assert(cdata.length == len, "ASN.1 internal error"); + + if (r->ek.encrypted_pa_data == NULL) { + ALLOC(r->ek.encrypted_pa_data); + if (r->ek.encrypted_pa_data == NULL) + return ENOMEM; + } + ret = krb5_padata_add(r->context, r->ek.encrypted_pa_data, + KRB5_PADATA_REQ_ENC_PA_REP, cdata.data, cdata.length); + if (ret) + return ret; + + if (!r->config->enable_fast) + return 0; + + return krb5_padata_add(r->context, r->ek.encrypted_pa_data, + KRB5_PADATA_FX_FAST, NULL, 0); +} + +/* + * Add an authorization data element indicating that a synthetic + * principal was used, so that the TGS does not accidentally + * synthesize a non-synthetic principal that has since been deleted. + */ +static krb5_error_code +add_synthetic_princ_ad(astgs_request_t r) +{ + krb5_data data; + + krb5_data_zero(&data); + + return _kdc_tkt_add_if_relevant_ad(r->context, &r->et, + KRB5_AUTHDATA_SYNTHETIC_PRINC_USED, + &data); +} + +static krb5_error_code +get_local_tgs(krb5_context context, + krb5_kdc_configuration *config, + krb5_const_realm realm, + HDB **krbtgtdb, + hdb_entry **krbtgt) +{ + krb5_error_code ret; + krb5_principal tgs_name; + + *krbtgtdb = NULL; + *krbtgt = NULL; + + ret = krb5_make_principal(context, + &tgs_name, + realm, + KRB5_TGS_NAME, + realm, + NULL); + if (ret == 0) + ret = _kdc_db_fetch(context, config, tgs_name, + HDB_F_GET_KRBTGT, NULL, krbtgtdb, krbtgt); + + krb5_free_principal(context, tgs_name); + return ret; +} + +/* + * + */ + +krb5_error_code +_kdc_as_rep(astgs_request_t r) +{ + krb5_kdc_configuration *config = r->config; + KDC_REQ *req = &r->req; + const char *from = r->from; + KDC_REQ_BODY *b = NULL; + KDC_REP *rep = &r->rep; + KDCOptions f; + krb5_enctype setype; + krb5_error_code ret = 0; + Key *skey; + int found_pa = 0; + int i, flags = HDB_F_FOR_AS_REQ; + const PA_DATA *pa; + krb5_boolean is_tgs; + const char *msg; + Key *krbtgt_key; + unsigned krbtgt_kvno; + + memset(rep, 0, sizeof(*rep)); + + ALLOC(rep->padata); + if (rep->padata == NULL) { + ret = ENOMEM; + krb5_set_error_message(r->context, ret, N_("malloc: out of memory", "")); + goto out; + } + + /* + * Look for FAST armor and unwrap + */ + ret = _kdc_fast_unwrap_request(r, NULL, NULL); + if (ret) { + _kdc_r_log(r, 1, "FAST unwrap request from %s failed: %d", from, ret); + goto out; + } + + /* Validate armor TGT, and initialize the armor client and PAC */ + if (r->armor_ticket) { + ret = _kdc_fast_check_armor_pac(r, HDB_F_FOR_AS_REQ); + if (ret) + goto out; + } + + b = &req->req_body; + f = b->kdc_options; + + if (f.canonicalize) + flags |= HDB_F_CANON; + + if (b->sname == NULL) { + ret = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; + _kdc_set_e_text(r, "No server in request"); + goto out; + } + + ret = _krb5_principalname2krb5_principal(r->context, &r->server_princ, + *(b->sname), b->realm); + if (!ret) + ret = krb5_unparse_name(r->context, r->server_princ, &r->sname); + if (ret) { + kdc_log(r->context, config, 2, + "AS_REQ malformed server name from %s", from); + goto out; + } + + if (b->cname == NULL) { + ret = KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN; + _kdc_set_e_text(r, "No client in request"); + goto out; + } + + ret = _krb5_principalname2krb5_principal(r->context, &r->client_princ, + *(b->cname), b->realm); + if (!ret) + ret = krb5_unparse_name(r->context, r->client_princ, &r->cname); + if (ret) { + kdc_log(r->context, config, 2, + "AS-REQ malformed client name from %s", from); + goto out; + } + + kdc_log(r->context, config, 4, "AS-REQ %s from %s for %s", + r->cname, r->from, r->sname); + + is_tgs = krb5_principal_is_krbtgt(r->context, r->server_princ); + + if (_kdc_is_anonymous(r->context, r->client_princ) && + !_kdc_is_anon_request(req)) { + kdc_log(r->context, config, 2, "Anonymous client w/o anonymous flag"); + ret = KRB5KDC_ERR_BADOPTION; + goto out; + } + + ret = _kdc_db_fetch(r->context, config, r->client_princ, + HDB_F_GET_CLIENT | HDB_F_SYNTHETIC_OK | flags, NULL, + &r->clientdb, &r->client); + switch (ret) { + case 0: /* Success */ + break; + case HDB_ERR_NOT_FOUND_HERE: + kdc_log(r->context, config, 5, "client %s does not have secrets at this KDC, need to proxy", + r->cname); + goto out; + case HDB_ERR_WRONG_REALM: { + char *fixed_client_name = NULL; + + ret = krb5_unparse_name(r->context, r->client->principal, + &fixed_client_name); + if (ret) { + goto out; + } + + kdc_log(r->context, config, 4, "WRONG_REALM - %s -> %s", + r->cname, fixed_client_name); + free(fixed_client_name); + + r->e_text = NULL; + ret = _kdc_fast_mk_error(r, r->rep.padata, r->armor_crypto, + &req->req_body, + r->error_code = KRB5_KDC_ERR_WRONG_REALM, + r->client->principal, r->server_princ, + NULL, NULL, r->reply); + goto out; + } + default: + { + msg = krb5_get_error_message(r->context, ret); + kdc_log(r->context, config, 4, "UNKNOWN -- %s: %s", r->cname, msg); + krb5_free_error_message(r->context, msg); + ret = KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN; + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_CLIENT_UNKNOWN); + goto out; + } + } + + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_CLIENT_FOUND); + + ret = _kdc_db_fetch(r->context, config, r->server_princ, + HDB_F_GET_SERVER | HDB_F_DELAY_NEW_KEYS | + flags | (is_tgs ? HDB_F_GET_KRBTGT : 0), + NULL, &r->serverdb, &r->server); + switch (ret) { + case 0: /* Success */ + break; + case HDB_ERR_NOT_FOUND_HERE: + kdc_log(r->context, config, 5, "target %s does not have secrets at this KDC, need to proxy", + r->sname); + goto out; + default: + msg = krb5_get_error_message(r->context, ret); + kdc_log(r->context, config, 4, "UNKNOWN -- %s: %s", r->sname, msg); + krb5_free_error_message(r->context, msg); + ret = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; + goto out; + } + + ret = _kdc_check_access(r); + if(ret) + goto out; + + /* + * This has to be here (not later), because we need to have r->sessionetype + * set prior to calling pa_pkinit_validate(), which in turn calls + * _kdc_pk_mk_pa_reply(), during padata validation. + */ + + /* + * Select an enctype for the to-be-issued ticket's session key using the + * intersection of the client's requested enctypes and the server's (like a + * root krbtgt, but not necessarily) etypes from its HDB entry. + */ + ret = _kdc_find_session_etype(r, b->etype.val, b->etype.len, + r->server, &r->sessionetype); + if (ret) { + kdc_log(r->context, config, 4, + "Client (%s) from %s has no common enctypes with KDC " + "to use for the session key", + r->cname, from); + goto out; + } + + /* + * Select the best encryption type for the KDC without regard to + * the client since the client never needs to read that data. + */ + + ret = _kdc_get_preferred_key(r->context, config, + r->server, r->sname, + &setype, &skey); + if(ret) + goto out; + + /* If server is not krbtgt, fetch local krbtgt key for signing authdata */ + if (is_tgs) { + krbtgt_key = skey; + krbtgt_kvno = r->server->kvno; + } else { + ret = get_local_tgs(r->context, config, r->server_princ->realm, + &r->krbtgtdb, &r->krbtgt); + if (ret) + goto out; + + ret = _kdc_get_preferred_key(r->context, config, r->krbtgt, + r->server_princ->realm, + NULL, &krbtgt_key); + if (ret) + goto out; + + krbtgt_kvno = r->server->kvno; + } + + /* + * Pre-auth processing + */ + + if(req->padata){ + unsigned int n; + + log_patypes(r, req->padata); + + /* Check if preauth matching */ + + for (n = 0; !found_pa && n < sizeof(pat) / sizeof(pat[0]); n++) { + if (pat[n].validate == NULL) + continue; + if (r->armor_crypto == NULL && (pat[n].flags & PA_REQ_FAST)) + continue; + if (!r->config->enable_fast_cookie && (pat[n].flags & PA_USES_FAST_COOKIE)) + continue; + + kdc_log(r->context, config, 5, + "Looking for %s pa-data -- %s", pat[n].name, r->cname); + i = 0; + pa = _kdc_find_padata(req, &i, pat[n].type); + if (pa) { + if (r->client->flags.synthetic && + !(pat[n].flags & PA_SYNTHETIC_OK)) { + kdc_log(r->context, config, 4, "UNKNOWN -- %s", r->cname); + ret = KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN; + goto out; + } + kdc_audit_addkv((kdc_request_t)r, KDC_AUDIT_VIS, "pa", "%s", + pat[n].name); + ret = pat[n].validate(r, pa); + if (ret != 0) { + krb5_error_code ret2; + Key *ckey = NULL; + krb5_boolean default_salt; + + if (ret != KRB5_KDC_ERR_MORE_PREAUTH_DATA_REQUIRED && + !kdc_audit_getkv((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT)) + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_PREAUTH_FAILED); + + /* + * If there is a client key, send ETYPE_INFO{,2} + */ + if (!r->client->flags.locked_out) { + ret2 = _kdc_find_etype(r, KFE_IS_PREAUTH|KFE_USE_CLIENT, + b->etype.val, b->etype.len, + NULL, &ckey, &default_salt); + if (ret2 == 0) { + ret2 = get_pa_etype_info_both(r->context, config, &b->etype, + r->rep.padata, ckey, !default_salt); + if (ret2 != 0) + ret = ret2; + } + } + goto out; + } + if (!kdc_audit_getkv((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT)) + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_PREAUTH_SUCCEEDED); + kdc_log(r->context, config, 4, + "%s pre-authentication succeeded -- %s", + pat[n].name, r->cname); + found_pa = 1; + r->pa_used = &pat[n]; + r->et.flags.pre_authent = 1; + } + } + } + + if (found_pa == 0) { + Key *ckey = NULL; + size_t n; + krb5_boolean default_salt; + + if (r->client->flags.synthetic) { + kdc_log(r->context, config, 4, "UNKNOWN -- %s", r->cname); + ret = KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN; + goto out; + } + + for (n = 0; n < sizeof(pat) / sizeof(pat[0]); n++) { + if ((pat[n].flags & PA_ANNOUNCE) == 0) + continue; + + if (!r->armor_crypto && (pat[n].flags & PA_REQ_FAST)) + continue; + if (pat[n].type == KRB5_PADATA_PKINIT_KX && !r->config->allow_anonymous) + continue; + if (pat[n].type == KRB5_PADATA_ENC_TIMESTAMP) { + if (r->armor_crypto && !r->config->enable_armored_pa_enc_timestamp) + continue; + if (!r->armor_crypto && !r->config->enable_unarmored_pa_enc_timestamp) + continue; + } + if (pat[n].type == KRB5_PADATA_FX_FAST && !r->config->enable_fast) + continue; + if (pat[n].type == KRB5_PADATA_GSS && !r->config->enable_gss_preauth) + continue; + if (!r->config->enable_fast_cookie && (pat[n].flags & PA_USES_FAST_COOKIE)) + continue; + + ret = krb5_padata_add(r->context, r->rep.padata, + pat[n].type, NULL, 0); + if (ret) + goto out; + } + + /* + * If there is a client key, send ETYPE_INFO{,2} + */ + ret = _kdc_find_etype(r, KFE_IS_PREAUTH|KFE_USE_CLIENT, + b->etype.val, b->etype.len, + NULL, &ckey, &default_salt); + if (ret == 0) { + ret = get_pa_etype_info_both(r->context, config, &b->etype, + r->rep.padata, ckey, !default_salt); + if (ret) + goto out; + } + + /* + * If the client indicated support for PKINIT Freshness, send back a + * freshness token. + */ + ret = send_freshness_token(r, krbtgt_key, krbtgt_kvno); + if (ret) + goto out; + + /* + * send requre preauth is its required or anon is requested, + * anon is today only allowed via preauth mechanisms. + */ + if (require_preauth_p(r) || _kdc_is_anon_request(&r->req)) { + ret = KRB5KDC_ERR_PREAUTH_REQUIRED; + _kdc_set_e_text(r, "Need to use PA-ENC-TIMESTAMP/PA-PK-AS-REQ"); + goto out; + } + + if (ckey == NULL) { + ret = KRB5KDC_ERR_CLIENT_NOTYET; + _kdc_set_e_text(r, "Doesn't have a client key available"); + goto out; + } + krb5_free_keyblock_contents(r->context, &r->reply_key); + ret = krb5_copy_keyblock_contents(r->context, &ckey->key, &r->reply_key); + if (ret) + goto out; + } + + r->canon_client_princ = r->client->principal; + + if (_kdc_is_anon_request(&r->req)) { + ret = _kdc_check_anon_policy(r); + if (ret) { + _kdc_set_e_text(r, "Anonymous ticket requests are disabled"); + goto out; + } + + r->et.flags.anonymous = 1; + } + + kdc_audit_setkv_number((kdc_request_t)r, KDC_REQUEST_KV_AUTH_EVENT, + KDC_AUTH_EVENT_CLIENT_AUTHORIZED); + + if(f.renew || f.validate || f.proxy || f.forwarded || f.enc_tkt_in_skey) { + ret = KRB5KDC_ERR_BADOPTION; + _kdc_set_e_text(r, "Bad KDC options"); + goto out; + } + + /* + * Build reply + */ + rep->pvno = 5; + rep->msg_type = krb_as_rep; + + if (!config->historical_anon_realm && + _kdc_is_anonymous(r->context, r->client_princ)) { + Realm anon_realm = KRB5_ANON_REALM; + ret = copy_Realm(&anon_realm, &rep->crealm); + } else if (f.canonicalize || r->client->flags.force_canonicalize) + ret = copy_Realm(&r->canon_client_princ->realm, &rep->crealm); + else + ret = copy_Realm(&r->client_princ->realm, &rep->crealm); + if (ret) + goto out; + if (r->et.flags.anonymous) + ret = _kdc_make_anonymous_principalname(&rep->cname); + else if (f.canonicalize || r->client->flags.force_canonicalize) + ret = _krb5_principal2principalname(&rep->cname, r->canon_client_princ); + else + ret = _krb5_principal2principalname(&rep->cname, r->client_princ); + if (ret) + goto out; + + rep->ticket.tkt_vno = 5; + if (f.canonicalize || r->server->flags.force_canonicalize) + ret = copy_Realm(&r->server->principal->realm, &rep->ticket.realm); + else + ret = copy_Realm(&r->server_princ->realm, &rep->ticket.realm); + if (ret) + goto out; + if (f.canonicalize || r->server->flags.force_canonicalize) + _krb5_principal2principalname(&rep->ticket.sname, + r->server->principal); + else + _krb5_principal2principalname(&rep->ticket.sname, + r->server_princ); + /* java 1.6 expects the name to be the same type, lets allow that + * uncomplicated name-types, when f.canonicalize is not set (to + * match Windows Server 1709). */ +#define CNT(sp,t) (((sp)->sname->name_type) == KRB5_NT_##t) + if (!f.canonicalize + && (CNT(b, UNKNOWN) || CNT(b, PRINCIPAL) || CNT(b, SRV_INST) || CNT(b, SRV_HST) || CNT(b, SRV_XHST))) { + rep->ticket.sname.name_type = b->sname->name_type; + } +#undef CNT + + r->et.flags.initial = 1; + if(r->client->flags.forwardable && r->server->flags.forwardable) + r->et.flags.forwardable = f.forwardable; + if(r->client->flags.proxiable && r->server->flags.proxiable) + r->et.flags.proxiable = f.proxiable; + else if (f.proxiable) { + _kdc_set_e_text(r, "Ticket may not be proxiable"); + ret = KRB5KDC_ERR_POLICY; + goto out; + } + if(r->client->flags.postdate && r->server->flags.postdate) + r->et.flags.may_postdate = f.allow_postdate; + else if (f.allow_postdate){ + _kdc_set_e_text(r, "Ticket may not be postdateable"); + ret = KRB5KDC_ERR_POLICY; + goto out; + } + + if (b->addresses) + kdc_audit_addaddrs((kdc_request_t)r, b->addresses, "reqaddrs"); + + /* check for valid set of addresses */ + if (!_kdc_check_addresses(r, b->addresses, r->addr)) { + if (r->config->warn_ticket_addresses) { + kdc_audit_setkv_bool((kdc_request_t)r, "wrongaddr", TRUE); + } else { + _kdc_set_e_text(r, "Request from wrong address"); + ret = KRB5KRB_AP_ERR_BADADDR; + goto out; + } + } + + ret = copy_PrincipalName(&rep->cname, &r->et.cname); + if (ret) + goto out; + ret = copy_Realm(&rep->crealm, &r->et.crealm); + if (ret) + goto out; + + { + time_t start; + time_t t; + + start = r->et.authtime = kdc_time; + + if(f.postdated && req->req_body.from){ + ALLOC(r->et.starttime); + start = *r->et.starttime = *req->req_body.from; + r->et.flags.invalid = 1; + r->et.flags.postdated = 1; /* XXX ??? */ + } + _kdc_fix_time(&b->till); + t = *b->till; + + /* be careful not to overflow */ + + /* + * Pre-auth can override r->client->max_life if configured. + * + * See pre-auth methods, specifically PKINIT, which can get or derive + * this from the client's certificate. + */ + if (r->pa_max_life > 0) + t = rk_time_add(start, min(rk_time_sub(t, start), r->pa_max_life)); + else if (r->client->max_life) + t = rk_time_add(start, min(rk_time_sub(t, start), + *r->client->max_life)); + + if (r->server->max_life) + t = rk_time_add(start, min(rk_time_sub(t, start), + *r->server->max_life)); + + /* Pre-auth can bound endtime as well */ + if (r->pa_endtime > 0) + t = rk_time_add(start, min(rk_time_sub(t, start), r->pa_endtime)); +#if 0 + t = min(t, rk_time_add(start, realm->max_life)); +#endif + r->et.endtime = t; + + if (start > r->et.endtime) { + _kdc_set_e_text(r, "Requested effective lifetime is negative or too short"); + ret = KRB5KDC_ERR_NEVER_VALID; + goto out; + } + + if(f.renewable_ok && r->et.endtime < *b->till){ + f.renewable = 1; + if(b->rtime == NULL){ + ALLOC(b->rtime); + *b->rtime = 0; + } + if(*b->rtime < *b->till) + *b->rtime = *b->till; + } + if(f.renewable && b->rtime){ + t = *b->rtime; + if(t == 0) + t = MAX_TIME; + if(r->client->max_renew) + t = rk_time_add(start, min(rk_time_sub(t, start), + *r->client->max_renew)); + if(r->server->max_renew) + t = rk_time_add(start, min(rk_time_sub(t, start), + *r->server->max_renew)); +#if 0 + t = min(t, rk_time_add(start, realm->max_renew)); +#endif + ALLOC(r->et.renew_till); + *r->et.renew_till = t; + r->et.flags.renewable = 1; + } + } + + if(b->addresses){ + ALLOC(r->et.caddr); + copy_HostAddresses(b->addresses, r->et.caddr); + } + + r->et.transited.tr_type = domain_X500_Compress; + krb5_data_zero(&r->et.transited.contents); + + /* The MIT ASN.1 library (obviously) doesn't tell lengths encoded + * as 0 and as 0x80 (meaning indefinite length) apart, and is thus + * incapable of correctly decoding SEQUENCE OF's of zero length. + * + * To fix this, always send at least one no-op last_req + * + * If there's a pw_end or valid_end we will use that, + * otherwise just a dummy lr. + */ + r->ek.last_req.val = malloc(2 * sizeof(*r->ek.last_req.val)); + if (r->ek.last_req.val == NULL) { + ret = ENOMEM; + goto out; + } + r->ek.last_req.len = 0; + if (r->client->pw_end + && (config->kdc_warn_pwexpire == 0 + || kdc_time + config->kdc_warn_pwexpire >= *r->client->pw_end)) { + r->ek.last_req.val[r->ek.last_req.len].lr_type = LR_PW_EXPTIME; + r->ek.last_req.val[r->ek.last_req.len].lr_value = *r->client->pw_end; + ++r->ek.last_req.len; + } + if (r->client->valid_end) { + r->ek.last_req.val[r->ek.last_req.len].lr_type = LR_ACCT_EXPTIME; + r->ek.last_req.val[r->ek.last_req.len].lr_value = *r->client->valid_end; + ++r->ek.last_req.len; + } + if (r->ek.last_req.len == 0) { + r->ek.last_req.val[r->ek.last_req.len].lr_type = LR_NONE; + r->ek.last_req.val[r->ek.last_req.len].lr_value = 0; + ++r->ek.last_req.len; + } + /* Set the nonce if it’s not already set. */ + if (!r->ek.nonce) { + r->ek.nonce = b->nonce; + } + if (r->client->valid_end || r->client->pw_end) { + ALLOC(r->ek.key_expiration); + if (r->client->valid_end) { + if (r->client->pw_end) + *r->ek.key_expiration = min(*r->client->valid_end, + *r->client->pw_end); + else + *r->ek.key_expiration = *r->client->valid_end; + } else + *r->ek.key_expiration = *r->client->pw_end; + } else + r->ek.key_expiration = NULL; + r->ek.flags = r->et.flags; + r->ek.authtime = r->et.authtime; + if (r->et.starttime) { + ALLOC(r->ek.starttime); + *r->ek.starttime = *r->et.starttime; + } + r->ek.endtime = r->et.endtime; + if (r->et.renew_till) { + ALLOC(r->ek.renew_till); + *r->ek.renew_till = *r->et.renew_till; + } + ret = copy_Realm(&rep->ticket.realm, &r->ek.srealm); + if (ret) + goto out; + ret = copy_PrincipalName(&rep->ticket.sname, &r->ek.sname); + if (ret) + goto out; + if(r->et.caddr){ + ALLOC(r->ek.caddr); + copy_HostAddresses(r->et.caddr, r->ek.caddr); + } + + /* + * Check session and reply keys + */ + + if (r->session_key.keytype == ETYPE_NULL) { + ret = krb5_generate_random_keyblock(r->context, r->sessionetype, &r->session_key); + if (ret) + goto out; + } + + if (r->reply_key.keytype == ETYPE_NULL) { + _kdc_set_e_text(r, "Client has no reply key"); + ret = KRB5KDC_ERR_CLIENT_NOTYET; + goto out; + } + + ret = copy_EncryptionKey(&r->session_key, &r->et.key); + if (ret) + goto out; + + ret = copy_EncryptionKey(&r->session_key, &r->ek.key); + if (ret) + goto out; + + /* Add the PAC */ + if (!r->et.flags.anonymous) { + ret = generate_pac(r, skey, krbtgt_key, is_tgs); + if (ret) + goto out; + } + + if (r->client->flags.synthetic) { + ret = add_synthetic_princ_ad(r); + if (ret) + goto out; + } + + _kdc_log_timestamp(r, "AS-REQ", r->et.authtime, + r->et.starttime, r->et.endtime, + r->et.renew_till); + + _log_astgs_req(r, setype); + + /* + * We always say we support FAST/enc-pa-rep + */ + + r->et.flags.enc_pa_rep = r->ek.flags.enc_pa_rep = 1; + + /* + * update reply-key with strengthen-key + */ + + ret = _kdc_fast_strengthen_reply_key(r); + if (ret) + goto out; + + /* + * Add REQ_ENC_PA_REP if client supports it + */ + + i = 0; + pa = _kdc_find_padata(req, &i, KRB5_PADATA_REQ_ENC_PA_REP); + if (pa) { + + ret = add_enc_pa_rep(r); + if (ret) { + msg = krb5_get_error_message(r->context, ret); + _kdc_r_log(r, 4, "add_enc_pa_rep failed: %s: %d", msg, ret); + krb5_free_error_message(r->context, msg); + goto out; + } + } + + /* + * Last chance for plugins to update reply + */ + ret = _kdc_finalize_reply(r); + if (ret) + goto out; + + /* + * Don't send kvno from client entry if the pre-authentication + * mechanism replaced the reply key. + */ + + ret = _kdc_encode_reply(r->context, config, + r, req->req_body.nonce, setype, + r->server->kvno, &skey->key, + pa_used_flag_isset(r, PA_REPLACE_REPLY_KEY) ? 0 : r->client->kvno, + 0, r->reply); + if (ret) + goto out; + + /* + * Check if message is too large + */ + if (r->datagram_reply && r->reply->length > config->max_datagram_reply_length) { + krb5_data_free(r->reply); + ret = KRB5KRB_ERR_RESPONSE_TOO_BIG; + _kdc_set_e_text(r, "Reply packet too large"); + } + +out: + if (ret) { + /* Overwrite ‘error_code’ only if we have an actual error. */ + r->error_code = ret; + } + { + krb5_error_code ret2 = _kdc_audit_request(r); + if (ret2) { + krb5_data_free(r->reply); + ret = ret2; + } + } + + /* + * In case of a non proxy error, build an error message. + */ + if (ret != 0 && ret != HDB_ERR_NOT_FOUND_HERE && r->reply->length == 0) { + kdc_log(r->context, config, 5, "as-req: sending error: %d to client", ret); + ret = _kdc_fast_mk_error(r, + r->rep.padata, + r->armor_crypto, + &req->req_body, + r->error_code ? r->error_code : ret, + r->client_princ, + r->server_princ, + NULL, NULL, + r->reply); + } + + if (r->pa_used && r->pa_used->cleanup) + r->pa_used->cleanup(r); + + free_AS_REP(&r->rep); + free_EncTicketPart(&r->et); + free_EncKDCRepPart(&r->ek); + _kdc_free_fast_state(&r->fast); + + if (r->client_princ) { + krb5_free_principal(r->context, r->client_princ); + r->client_princ = NULL; + } + if (r->server_princ){ + krb5_free_principal(r->context, r->server_princ); + r->server_princ = NULL; + } + if (r->client) + _kdc_free_ent(r->context, r->clientdb, r->client); + if (r->server) + _kdc_free_ent(r->context, r->serverdb, r->server); + if (r->krbtgt) + _kdc_free_ent(r->context, r->krbtgtdb, r->krbtgt); + if (r->armor_crypto) { + krb5_crypto_destroy(r->context, r->armor_crypto); + r->armor_crypto = NULL; + } + if (r->armor_ticket) + krb5_free_ticket(r->context, r->armor_ticket); + if (r->armor_server) + _kdc_free_ent(r->context, r->armor_serverdb, r->armor_server); + krb5_free_keyblock_contents(r->context, &r->reply_key); + krb5_free_keyblock_contents(r->context, &r->enc_ad_key); + krb5_free_keyblock_contents(r->context, &r->session_key); + krb5_free_keyblock_contents(r->context, &r->strengthen_key); + krb5_pac_free(r->context, r->pac); + + return ret; +} diff --git a/third_party/heimdal/kdc/krb5tgs.c b/third_party/heimdal/kdc/krb5tgs.c new file mode 100644 index 0000000..d744f56 --- /dev/null +++ b/third_party/heimdal/kdc/krb5tgs.c @@ -0,0 +1,2321 @@ +/* + * Copyright (c) 1997-2008 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. + */ + +#include "kdc_locl.h" + +/* + * return the realm of a krbtgt-ticket or NULL + */ + +static Realm +get_krbtgt_realm(const PrincipalName *p) +{ + if(p->name_string.len == 2 + && strcmp(p->name_string.val[0], KRB5_TGS_NAME) == 0) + return p->name_string.val[1]; + else + return NULL; +} + +/* + * return TRUE if client was a synthetic principal, as indicated by + * authorization data + */ +krb5_boolean +_kdc_synthetic_princ_used_p(krb5_context context, krb5_ticket *ticket) +{ + krb5_data synthetic_princ_used; + krb5_error_code ret; + + ret = krb5_ticket_get_authorization_data_type(context, ticket, + KRB5_AUTHDATA_SYNTHETIC_PRINC_USED, + &synthetic_princ_used); + if (ret == ENOENT) + ret = krb5_ticket_get_authorization_data_type(context, ticket, + KRB5_AUTHDATA_INITIAL_VERIFIED_CAS, + &synthetic_princ_used); + + if (ret == 0) + krb5_data_free(&synthetic_princ_used); + + return ret == 0; +} + +/* + * + */ + +krb5_error_code +_kdc_check_pac(astgs_request_t r, + const krb5_principal client_principal, + hdb_entry *delegated_proxy, + hdb_entry *client, + hdb_entry *server, + hdb_entry *krbtgt, + hdb_entry *ticket_server, + const EncryptionKey *server_check_key, + const EncryptionKey *krbtgt_check_key, + EncTicketPart *tkt, + krb5_boolean *kdc_issued, + krb5_pac *ppac, + krb5_principal *pac_canon_name, + uint64_t *pac_attributes) +{ + krb5_context context = r->context; + krb5_kdc_configuration *config = r->config; + krb5_pac pac = NULL; + krb5_error_code ret; + krb5_boolean signedticket; + + *kdc_issued = FALSE; + *ppac = NULL; + if (pac_canon_name) + *pac_canon_name = NULL; + if (pac_attributes) + *pac_attributes = KRB5_PAC_WAS_GIVEN_IMPLICITLY; + + ret = _krb5_kdc_pac_ticket_parse(context, tkt, &signedticket, &pac); + if (ret) + return ret; + + if (pac == NULL) { + if (config->require_pac) + ret = KRB5KDC_ERR_TGT_REVOKED; + return ret; + } + + /* Verify the server signature. */ + ret = krb5_pac_verify(context, pac, tkt->authtime, client_principal, + server_check_key, NULL); + if (ret) { + krb5_pac_free(context, pac); + return ret; + } + + /* Verify the KDC signatures. */ + ret = _kdc_pac_verify(r, + client_principal, delegated_proxy, + client, server, krbtgt, tkt, pac); + if (ret == 0) { + if (pac_canon_name) { + ret = _krb5_pac_get_canon_principal(context, pac, pac_canon_name); + if (ret && ret != ENOENT) { + krb5_pac_free(context, pac); + return ret; + } + } + if (pac_attributes && + _krb5_pac_get_attributes_info(context, pac, pac_attributes) != 0) + *pac_attributes = KRB5_PAC_WAS_GIVEN_IMPLICITLY; + } else if (ret == KRB5_PLUGIN_NO_HANDLE) { + /* + * We can't verify the KDC signatures if the ticket was issued by + * another realm's KDC. + */ + if (krb5_realm_compare(context, server->principal, + ticket_server->principal)) { + ret = krb5_pac_verify(context, pac, 0, NULL, NULL, + krbtgt_check_key); + if (ret) { + krb5_pac_free(context, pac); + return ret; + } + } + + if (pac_canon_name) { + ret = _krb5_pac_get_canon_principal(context, pac, pac_canon_name); + if (ret && ret != ENOENT) { + krb5_pac_free(context, pac); + return ret; + } + } + if (pac_attributes && + _krb5_pac_get_attributes_info(context, pac, pac_attributes) != 0) + *pac_attributes = KRB5_PAC_WAS_GIVEN_IMPLICITLY; + + /* Discard the PAC if the plugin didn't handle it */ + krb5_pac_free(context, pac); + ret = krb5_pac_init(context, &pac); + if (ret) + return ret; + } else { + krb5_pac_free(context, pac); + return ret; + } + + *kdc_issued = signedticket || + krb5_principal_is_krbtgt(context, + ticket_server->principal); + *ppac = pac; + + return 0; +} + +static krb5_boolean +is_anon_tgs_request_p(const KDC_REQ_BODY *b, + const EncTicketPart *tgt) +{ + KDCOptions f = b->kdc_options; + + /* + * Versions of Heimdal from 1.0 to 7.6, inclusive, send both the + * request-anonymous and cname-in-addl-tkt flags for constrained + * delegation requests. A true anonymous TGS request will only + * have the request-anonymous flag set. (A corollary of this is + * that it is not possible to support anonymous constrained + * delegation requests, although they would be of limited utility.) + */ + return tgt->flags.anonymous || + (f.request_anonymous && !f.cname_in_addl_tkt && !b->additional_tickets); +} + +/* + * + */ + +static krb5_error_code +check_tgs_flags(astgs_request_t r, KDC_REQ_BODY *b, + krb5_const_principal tgt_name, + const EncTicketPart *tgt, EncTicketPart *et) +{ + KDCOptions f = b->kdc_options; + + if(f.validate){ + if (!tgt->flags.invalid || tgt->starttime == NULL) { + kdc_audit_addreason((kdc_request_t)r, + "Bad request to validate ticket"); + return KRB5KDC_ERR_BADOPTION; + } + if(*tgt->starttime > kdc_time){ + kdc_audit_addreason((kdc_request_t)r, + "Early request to validate ticket"); + return KRB5KRB_AP_ERR_TKT_NYV; + } + /* XXX tkt = tgt */ + et->flags.invalid = 0; + } else if (tgt->flags.invalid) { + kdc_audit_addreason((kdc_request_t)r, + "Ticket-granting ticket has INVALID flag set"); + return KRB5KRB_AP_ERR_TKT_INVALID; + } + + if(f.forwardable){ + if (!tgt->flags.forwardable) { + kdc_audit_addreason((kdc_request_t)r, + "Bad request for forwardable ticket"); + return KRB5KDC_ERR_BADOPTION; + } + et->flags.forwardable = 1; + } + if(f.forwarded){ + if (!tgt->flags.forwardable) { + kdc_audit_addreason((kdc_request_t)r, + "Request to forward non-forwardable ticket"); + return KRB5KDC_ERR_BADOPTION; + } + et->flags.forwarded = 1; + et->caddr = b->addresses; + } + if(tgt->flags.forwarded) + et->flags.forwarded = 1; + + if(f.proxiable){ + if (!tgt->flags.proxiable) { + kdc_audit_addreason((kdc_request_t)r, + "Bad request for proxiable ticket"); + return KRB5KDC_ERR_BADOPTION; + } + et->flags.proxiable = 1; + } + if(f.proxy){ + if (!tgt->flags.proxiable) { + kdc_audit_addreason((kdc_request_t)r, + "Request to proxy non-proxiable ticket"); + return KRB5KDC_ERR_BADOPTION; + } + et->flags.proxy = 1; + et->caddr = b->addresses; + } + if(tgt->flags.proxy) + et->flags.proxy = 1; + + if(f.allow_postdate){ + if (!tgt->flags.may_postdate) { + kdc_audit_addreason((kdc_request_t)r, + "Bad request for post-datable ticket"); + return KRB5KDC_ERR_BADOPTION; + } + et->flags.may_postdate = 1; + } + if(f.postdated){ + if (!tgt->flags.may_postdate) { + kdc_audit_addreason((kdc_request_t)r, + "Bad request for postdated ticket"); + return KRB5KDC_ERR_BADOPTION; + } + if(b->from) + *et->starttime = *b->from; + et->flags.postdated = 1; + et->flags.invalid = 1; + } else if (b->from && *b->from > kdc_time + r->context->max_skew) { + kdc_audit_addreason((kdc_request_t)r, + "Ticket cannot be postdated"); + return KRB5KDC_ERR_CANNOT_POSTDATE; + } + + if(f.renewable){ + if (!tgt->flags.renewable || tgt->renew_till == NULL) { + kdc_audit_addreason((kdc_request_t)r, + "Bad request for renewable ticket"); + return KRB5KDC_ERR_BADOPTION; + } + et->flags.renewable = 1; + ALLOC(et->renew_till); + _kdc_fix_time(&b->rtime); + *et->renew_till = *b->rtime; + } + if(f.renew){ + time_t old_life; + if (!tgt->flags.renewable || tgt->renew_till == NULL) { + kdc_audit_addreason((kdc_request_t)r, + "Request to renew non-renewable ticket"); + return KRB5KDC_ERR_BADOPTION; + } + old_life = tgt->endtime; + if(tgt->starttime) + old_life -= *tgt->starttime; + else + old_life -= tgt->authtime; + et->endtime = *et->starttime + old_life; + if (et->renew_till != NULL) + et->endtime = min(*et->renew_till, et->endtime); + } + + /* + * RFC 8062 section 3 defines an anonymous ticket as one containing + * the anonymous principal and the anonymous ticket flag. + */ + if (tgt->flags.anonymous && + !_kdc_is_anonymous(r->context, tgt_name)) { + kdc_audit_addreason((kdc_request_t)r, + "Anonymous ticket flag set without " + "anonymous principal"); + return KRB5KDC_ERR_BADOPTION; + } + + /* + * RFC 8062 section 4.2 states that if the TGT is anonymous, the + * anonymous KDC option SHOULD be set, but it is not required. + * Treat an anonymous TGT as if the anonymous flag was set. + */ + if (is_anon_tgs_request_p(b, tgt)) + et->flags.anonymous = 1; + + return 0; +} + +/* + * Determine if s4u2self is allowed from this client to this server + * + * also: + * + * Check that the client (user2user TGT, enc-tkt-in-skey) hosts the + * service given by the client. + * + * For example, regardless of the principal being impersonated, if the + * 'client' and 'server' (target) are the same, or server is an SPN + * alias of client, then it's safe. + */ + +krb5_error_code +_kdc_check_client_matches_target_service(krb5_context context, + krb5_kdc_configuration *config, + HDB *clientdb, + hdb_entry *client, + hdb_entry *target_server, + krb5_const_principal target_server_principal) +{ + krb5_error_code ret; + + /* + * Always allow the plugin to check, this might be faster, allow a + * policy or audit check and can look into the DB records + * directly + */ + if (clientdb->hdb_check_client_matches_target_service) { + ret = clientdb->hdb_check_client_matches_target_service(context, + clientdb, + client, + target_server); + if (ret == 0) + return 0; + } else if (krb5_principal_compare(context, + client->principal, + target_server_principal) == TRUE) { + /* if client does a s4u2self to itself, and there is no plugin, that is ok */ + return 0; + } else { + ret = KRB5KDC_ERR_BADOPTION; + } + return ret; +} + +/* + * + */ + +krb5_error_code +_kdc_verify_flags(krb5_context context, + krb5_kdc_configuration *config, + const EncTicketPart *et, + const char *pstr) +{ + if(et->endtime < kdc_time){ + kdc_log(context, config, 4, "Ticket expired (%s)", pstr); + return KRB5KRB_AP_ERR_TKT_EXPIRED; + } + if(et->flags.invalid){ + kdc_log(context, config, 4, "Ticket not valid (%s)", pstr); + return KRB5KRB_AP_ERR_TKT_NYV; + } + return 0; +} + +/* + * + */ + +static krb5_error_code +fix_transited_encoding(krb5_context context, + krb5_kdc_configuration *config, + krb5_boolean check_policy, + const TransitedEncoding *tr, + EncTicketPart *et, + const char *client_realm, + const char *server_realm, + const char *tgt_realm) +{ + krb5_error_code ret = 0; + char **realms, **tmp; + unsigned int num_realms; + size_t i; + + switch (tr->tr_type) { + case domain_X500_Compress: + break; + case 0: + /* + * Allow empty content of type 0 because that is was Microsoft + * generates in their TGT. + */ + if (tr->contents.length == 0) + break; + kdc_log(context, config, 4, + "Transited type 0 with non empty content"); + return KRB5KDC_ERR_TRTYPE_NOSUPP; + default: + kdc_log(context, config, 4, + "Unknown transited type: %u", tr->tr_type); + return KRB5KDC_ERR_TRTYPE_NOSUPP; + } + + ret = krb5_domain_x500_decode(context, + tr->contents, + &realms, + &num_realms, + client_realm, + server_realm); + if(ret){ + krb5_warn(context, ret, + "Decoding transited encoding"); + return ret; + } + + /* + * If the realm of the presented tgt is neither the client nor the server + * realm, it is a transit realm and must be added to transited set. + */ + if (strcmp(client_realm, tgt_realm) != 0 && + strcmp(server_realm, tgt_realm) != 0) { + if (num_realms + 1 > UINT_MAX/sizeof(*realms)) { + ret = ERANGE; + goto free_realms; + } + tmp = realloc(realms, (num_realms + 1) * sizeof(*realms)); + if(tmp == NULL){ + ret = ENOMEM; + goto free_realms; + } + realms = tmp; + realms[num_realms] = strdup(tgt_realm); + if(realms[num_realms] == NULL){ + ret = ENOMEM; + goto free_realms; + } + num_realms++; + } + if(num_realms == 0) { + if (strcmp(client_realm, server_realm) != 0) + kdc_log(context, config, 4, + "cross-realm %s -> %s", client_realm, server_realm); + } else { + size_t l = 0; + char *rs; + for(i = 0; i < num_realms; i++) + l += strlen(realms[i]) + 2; + rs = malloc(l); + if(rs != NULL) { + *rs = '\0'; + for(i = 0; i < num_realms; i++) { + if(i > 0) + strlcat(rs, ", ", l); + strlcat(rs, realms[i], l); + } + kdc_log(context, config, 4, + "cross-realm %s -> %s via [%s]", + client_realm, server_realm, rs); + free(rs); + } + } + if(check_policy) { + ret = krb5_check_transited(context, client_realm, + server_realm, + realms, num_realms, NULL); + if(ret) { + krb5_warn(context, ret, "cross-realm %s -> %s", + client_realm, server_realm); + goto free_realms; + } + et->flags.transited_policy_checked = 1; + } + et->transited.tr_type = domain_X500_Compress; + ret = krb5_domain_x500_encode(realms, num_realms, &et->transited.contents); + if(ret) + krb5_warn(context, ret, "Encoding transited encoding"); + free_realms: + for(i = 0; i < num_realms; i++) + free(realms[i]); + free(realms); + return ret; +} + + +static krb5_error_code +tgs_make_reply(astgs_request_t r, + const EncTicketPart *tgt, + const EncryptionKey *serverkey, + const EncryptionKey *krbtgtkey, + const krb5_keyblock *sessionkey, + krb5_kvno kvno, + AuthorizationData *auth_data, + const char *tgt_realm, + uint16_t rodc_id, + krb5_boolean add_ticket_sig) +{ + KDC_REQ_BODY *b = &r->req.req_body; + krb5_data *reply = r->reply; + KDC_REP *rep = &r->rep; + EncTicketPart *et = &r->et; + EncKDCRepPart *ek = &r->ek; + KDCOptions f = b->kdc_options; + krb5_error_code ret; + int is_weak = 0; + + heim_assert(r->client_princ != NULL, "invalid client name passed to tgs_make_reply"); + + rep->pvno = 5; + rep->msg_type = krb_tgs_rep; + + if (et->authtime == 0) + et->authtime = tgt->authtime; + _kdc_fix_time(&b->till); + et->endtime = min(tgt->endtime, *b->till); + ALLOC(et->starttime); + *et->starttime = kdc_time; + + ret = check_tgs_flags(r, b, r->client_princ, tgt, et); + if(ret) + goto out; + + /* We should check the transited encoding if: + 1) the request doesn't ask not to be checked + 2) globally enforcing a check + 3) principal requires checking + 4) we allow non-check per-principal, but principal isn't marked as allowing this + 5) we don't globally allow this + */ + +#define GLOBAL_FORCE_TRANSITED_CHECK \ + (r->config->trpolicy == TRPOLICY_ALWAYS_CHECK) +#define GLOBAL_ALLOW_PER_PRINCIPAL \ + (r->config->trpolicy == TRPOLICY_ALLOW_PER_PRINCIPAL) +#define GLOBAL_ALLOW_DISABLE_TRANSITED_CHECK \ + (r->config->trpolicy == TRPOLICY_ALWAYS_HONOUR_REQUEST) + +/* these will consult the database in future release */ +#define PRINCIPAL_FORCE_TRANSITED_CHECK(P) 0 +#define PRINCIPAL_ALLOW_DISABLE_TRANSITED_CHECK(P) 0 + + ret = fix_transited_encoding(r->context, r->config, + !f.disable_transited_check || + GLOBAL_FORCE_TRANSITED_CHECK || + PRINCIPAL_FORCE_TRANSITED_CHECK(r->server) || + !((GLOBAL_ALLOW_PER_PRINCIPAL && + PRINCIPAL_ALLOW_DISABLE_TRANSITED_CHECK(r->server)) || + GLOBAL_ALLOW_DISABLE_TRANSITED_CHECK), + &tgt->transited, et, + krb5_principal_get_realm(r->context, r->client_princ), + krb5_principal_get_realm(r->context, r->server->principal), + tgt_realm); + + { + /* + * RFC 6806 notes that names MUST NOT be changed in the response to a + * TGS request. Hence we ignore the setting of the canonicalize KDC + * option. However, for legacy interoperability we do allow the backend + * to override this by setting the force-canonicalize HDB flag in the + * server entry. + */ + krb5_const_principal rsp; + + if (r->server->flags.force_canonicalize) + rsp = r->server->principal; + else + rsp = r->server_princ; + if (ret == 0) + ret = copy_Realm(&rsp->realm, &rep->ticket.realm); + if (ret == 0) + ret = _krb5_principal2principalname(&rep->ticket.sname, rsp); + } + + if (ret == 0) + ret = copy_Realm(&r->client_princ->realm, &rep->crealm); + if (ret) + goto out; + + /* + * RFC 8062 states "if the ticket in the TGS request is an anonymous + * one, the client and client realm are copied from that ticket". So + * whilst the TGT flag check below is superfluous, it is included in + * order to follow the specification to its letter. + */ + if (et->flags.anonymous && !tgt->flags.anonymous) + _kdc_make_anonymous_principalname(&rep->cname); + else + ret = copy_PrincipalName(&r->client_princ->name, &rep->cname); + if (ret) + goto out; + rep->ticket.tkt_vno = 5; + + ek->caddr = et->caddr; + + { + time_t life; + life = et->endtime - *et->starttime; + if(r->client && r->client->max_life) + life = min(life, *r->client->max_life); + if(r->server->max_life) + life = min(life, *r->server->max_life); + et->endtime = *et->starttime + life; + } + if(f.renewable_ok && tgt->flags.renewable && + et->renew_till == NULL && et->endtime < *b->till && + tgt->renew_till != NULL) + { + et->flags.renewable = 1; + ALLOC(et->renew_till); + *et->renew_till = *b->till; + } + if(et->renew_till){ + time_t renew; + renew = *et->renew_till - *et->starttime; + if(r->client && r->client->max_renew) + renew = min(renew, *r->client->max_renew); + if(r->server->max_renew) + renew = min(renew, *r->server->max_renew); + *et->renew_till = *et->starttime + renew; + } + + if(et->renew_till){ + *et->renew_till = min(*et->renew_till, *tgt->renew_till); + *et->starttime = min(*et->starttime, *et->renew_till); + et->endtime = min(et->endtime, *et->renew_till); + } + + *et->starttime = min(*et->starttime, et->endtime); + + if(*et->starttime == et->endtime){ + ret = KRB5KDC_ERR_NEVER_VALID; + goto out; + } + if(et->renew_till && et->endtime == *et->renew_till){ + free(et->renew_till); + et->renew_till = NULL; + et->flags.renewable = 0; + } + + et->flags.pre_authent = tgt->flags.pre_authent; + et->flags.hw_authent = tgt->flags.hw_authent; + et->flags.ok_as_delegate = r->server->flags.ok_as_delegate; + + /* See MS-KILE 3.3.5.1 */ + if (!r->server->flags.forwardable) + et->flags.forwardable = 0; + if (!r->server->flags.proxiable) + et->flags.proxiable = 0; + + if (auth_data) { + unsigned int i = 0; + + /* XXX check authdata */ + + if (et->authorization_data == NULL) { + et->authorization_data = calloc(1, sizeof(*et->authorization_data)); + if (et->authorization_data == NULL) { + ret = ENOMEM; + krb5_set_error_message(r->context, ret, "malloc: out of memory"); + goto out; + } + } + for(i = 0; i < auth_data->len ; i++) { + ret = add_AuthorizationData(et->authorization_data, &auth_data->val[i]); + if (ret) { + krb5_set_error_message(r->context, ret, "malloc: out of memory"); + goto out; + } + } + } + + ret = krb5_copy_keyblock_contents(r->context, sessionkey, &et->key); + if (ret) + goto out; + et->crealm = rep->crealm; + et->cname = rep->cname; + + ek->key = et->key; + /* MIT must have at least one last_req */ + ek->last_req.val = calloc(1, sizeof(*ek->last_req.val)); + if (ek->last_req.val == NULL) { + ret = ENOMEM; + goto out; + } + ek->last_req.len = 1; /* set after alloc to avoid null deref on cleanup */ + ek->nonce = b->nonce; + ek->flags = et->flags; + ek->authtime = et->authtime; + ek->starttime = et->starttime; + ek->endtime = et->endtime; + ek->renew_till = et->renew_till; + ek->srealm = rep->ticket.realm; + ek->sname = rep->ticket.sname; + + _kdc_log_timestamp(r, "TGS-REQ", et->authtime, et->starttime, + et->endtime, et->renew_till); + + if (krb5_enctype_valid(r->context, serverkey->keytype) != 0 + && _kdc_is_weak_exception(r->server->principal, serverkey->keytype)) + { + krb5_enctype_enable(r->context, serverkey->keytype); + is_weak = 1; + } + + if (r->canon_client_princ) { + char *cpn; + + (void) krb5_unparse_name(r->context, r->canon_client_princ, &cpn); + kdc_audit_addkv((kdc_request_t)r, 0, "canon_client_name", "%s", + cpn ? cpn : "<unknown>"); + krb5_xfree(cpn); + } + + /* + * For anonymous tickets, we should filter out positive authorization data + * that could reveal the client's identity, and return a policy error for + * restrictive authorization data. Policy for unknown authorization types + * is implementation dependent. + */ + if (r->pac && !et->flags.anonymous) { + kdc_audit_setkv_number((kdc_request_t)r, "pac_attributes", + r->pac_attributes); + + /* + * PACs are included when issuing TGTs, if there is no PAC_ATTRIBUTES + * buffer (legacy behavior) or if the attributes buffer indicates the + * AS client requested one. + */ + if (_kdc_include_pac_p(r)) { + krb5_boolean is_tgs = + krb5_principal_is_krbtgt(r->context, r->server->principal); + + ret = _krb5_kdc_pac_sign_ticket(r->context, r->pac, r->client_princ, serverkey, + krbtgtkey, rodc_id, NULL, r->canon_client_princ, + add_ticket_sig, add_ticket_sig, et, + is_tgs ? &r->pac_attributes : NULL); + if (ret) + goto out; + } + } + + ret = _kdc_finalize_reply(r); + if (ret) + goto out; + + /* It is somewhat unclear where the etype in the following + encryption should come from. What we have is a session + key in the passed tgt, and a list of preferred etypes + *for the new ticket*. Should we pick the best possible + etype, given the keytype in the tgt, or should we look + at the etype list here as well? What if the tgt + session key is DES3 and we want a ticket with a (say) + CAST session key. Should the DES3 etype be added to the + etype list, even if we don't want a session key with + DES3? */ + ret = _kdc_encode_reply(r->context, r->config, r, b->nonce, + serverkey->keytype, kvno, + serverkey, 0, r->rk_is_subkey, reply); + if (is_weak) + krb5_enctype_disable(r->context, serverkey->keytype); + + _log_astgs_req(r, serverkey->keytype); + +out: + return ret; +} + +static krb5_error_code +tgs_check_authenticator(krb5_context context, + krb5_kdc_configuration *config, + krb5_auth_context ac, + KDC_REQ_BODY *b, + krb5_keyblock *key) +{ + krb5_authenticator auth; + krb5_error_code ret; + krb5_crypto crypto; + + ret = krb5_auth_con_getauthenticator(context, ac, &auth); + if (ret) { + kdc_log(context, config, 2, + "Out of memory checking PA-TGS Authenticator"); + goto out; + } + if(auth->cksum == NULL){ + kdc_log(context, config, 4, "No authenticator in request"); + ret = KRB5KRB_AP_ERR_INAPP_CKSUM; + goto out; + } + + if (!krb5_checksum_is_collision_proof(context, auth->cksum->cksumtype)) { + kdc_log(context, config, 4, "Bad checksum type in authenticator: %d", + auth->cksum->cksumtype); + ret = KRB5KRB_AP_ERR_INAPP_CKSUM; + goto out; + } + + ret = krb5_crypto_init(context, key, 0, &crypto); + if (ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 4, "krb5_crypto_init failed: %s", msg); + krb5_free_error_message(context, msg); + goto out; + } + + /* + * RFC4120 says the checksum must be collision-proof, but it does + * not require it to be keyed (as the authenticator is encrypted). + */ + _krb5_crypto_set_flags(context, crypto, KRB5_CRYPTO_FLAG_ALLOW_UNKEYED_CHECKSUM); + ret = _kdc_verify_checksum(context, + crypto, + KRB5_KU_TGS_REQ_AUTH_CKSUM, + &b->_save, + auth->cksum); + krb5_crypto_destroy(context, crypto); + if(ret){ + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 4, + "Failed to verify authenticator checksum: %s", msg); + krb5_free_error_message(context, msg); + } +out: + free_Authenticator(auth); + free(auth); + return ret; +} + +static krb5_boolean +need_referral(krb5_context context, krb5_kdc_configuration *config, + const KDCOptions * const options, krb5_principal server, + krb5_realm **realms) +{ + const char *name; + + if(!options->canonicalize && server->name.name_type != KRB5_NT_SRV_INST) + return FALSE; + + if (server->name.name_string.len == 1) + name = server->name.name_string.val[0]; + else if (server->name.name_string.len > 1) + name = server->name.name_string.val[1]; + else + return FALSE; + + kdc_log(context, config, 5, "Searching referral for %s", name); + + return _krb5_get_host_realm_int(context, name, FALSE, realms) == 0; +} + +static krb5_error_code +validate_fast_ad(astgs_request_t r, krb5_authdata *auth_data) +{ + krb5_error_code ret; + krb5_data data; + + krb5_data_zero(&data); + + if (!r->config->enable_fast) + return 0; + + ret = _krb5_get_ad(r->context, auth_data, NULL, + KRB5_AUTHDATA_FX_FAST_USED, &data); + if (ret == 0) { + r->fast_asserted = 1; + krb5_data_free(&data); + } + + ret = _krb5_get_ad(r->context, auth_data, NULL, + KRB5_AUTHDATA_FX_FAST_ARMOR, &data); + if (ret == 0) { + kdc_log(r->context, r->config, 2, + "Invalid ticket usage: TGS-REQ contains AD-fx-fast-armor"); + krb5_data_free(&data); + return KRB5KRB_AP_ERR_BAD_INTEGRITY; + } + + return 0; +} + +static krb5_error_code +tgs_parse_request(astgs_request_t r, + const PA_DATA *tgs_req, + krb5_enctype *krbtgt_etype, + const char *from, + const struct sockaddr *from_addr, + time_t **csec, + int **cusec) +{ + krb5_kdc_configuration *config = r->config; + KDC_REQ_BODY *b = &r->req.req_body; + static char failed[] = "<unparse_name failed>"; + krb5_ap_req ap_req; + krb5_error_code ret; + krb5_principal princ; + krb5_auth_context ac = NULL; + krb5_flags ap_req_options; + krb5_flags verify_ap_req_flags = 0; + krb5uint32 krbtgt_kvno; /* kvno used for the PA-TGS-REQ AP-REQ Ticket */ + krb5uint32 krbtgt_kvno_try; + int kvno_search_tries = 4; /* number of kvnos to try when tkt_vno == 0 */ + const Keys *krbtgt_keys;/* keyset for TGT tkt_vno */ + Key *tkey; + krb5_keyblock *subkey = NULL; + + *csec = NULL; + *cusec = NULL; + + memset(&ap_req, 0, sizeof(ap_req)); + ret = krb5_decode_ap_req(r->context, &tgs_req->padata_value, &ap_req); + if(ret){ + const char *msg = krb5_get_error_message(r->context, ret); + kdc_log(r->context, config, 4, "Failed to decode AP-REQ: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + if(!krb5_principalname_is_krbtgt(r->context, &ap_req.ticket.sname)){ + /* + * Note: this check is not to be depended upon for security. Nothing + * prevents a client modifying the sname, as it is located in the + * unencrypted part of the ticket. + */ + + /* XXX check for ticket.sname == req.sname */ + kdc_log(r->context, config, 4, "PA-DATA is not a ticket-granting ticket"); + ret = KRB5KDC_ERR_POLICY; /* ? */ + goto out; + } + + _krb5_principalname2krb5_principal(r->context, + &princ, + ap_req.ticket.sname, + ap_req.ticket.realm); + + krbtgt_kvno = ap_req.ticket.enc_part.kvno ? *ap_req.ticket.enc_part.kvno : 0; + ret = _kdc_db_fetch(r->context, config, princ, HDB_F_GET_KRBTGT, + &krbtgt_kvno, &r->krbtgtdb, &r->krbtgt); + + if (ret == HDB_ERR_NOT_FOUND_HERE) { + /* XXX Factor out this unparsing of the same princ all over */ + char *p; + ret = krb5_unparse_name(r->context, princ, &p); + if (ret != 0) + p = failed; + krb5_free_principal(r->context, princ); + kdc_log(r->context, config, 5, + "Ticket-granting ticket account %s does not have secrets at " + "this KDC, need to proxy", p); + if (ret == 0) + free(p); + ret = HDB_ERR_NOT_FOUND_HERE; + goto out; + } else if (ret == HDB_ERR_KVNO_NOT_FOUND) { + char *p; + ret = krb5_unparse_name(r->context, princ, &p); + if (ret != 0) + p = failed; + krb5_free_principal(r->context, princ); + kdc_log(r->context, config, 5, + "Ticket-granting ticket account %s does not have keys for " + "kvno %d at this KDC", p, krbtgt_kvno); + if (ret == 0) + free(p); + ret = HDB_ERR_KVNO_NOT_FOUND; + goto out; + } else if (ret == HDB_ERR_NO_MKEY) { + char *p; + ret = krb5_unparse_name(r->context, princ, &p); + if (ret != 0) + p = failed; + krb5_free_principal(r->context, princ); + kdc_log(r->context, config, 5, + "Missing master key for decrypting keys for ticket-granting " + "ticket account %s with kvno %d at this KDC", p, krbtgt_kvno); + if (ret == 0) + free(p); + ret = HDB_ERR_KVNO_NOT_FOUND; + goto out; + } else if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + char *p; + ret = krb5_unparse_name(r->context, princ, &p); + if (ret != 0) + p = failed; + kdc_log(r->context, config, 4, + "Ticket-granting ticket %s not found in database: %s", p, msg); + krb5_free_principal(r->context, princ); + krb5_free_error_message(r->context, msg); + if (ret == 0) + free(p); + ret = KRB5KRB_AP_ERR_NOT_US; + goto out; + } + + krbtgt_kvno_try = krbtgt_kvno ? krbtgt_kvno : r->krbtgt->kvno; + *krbtgt_etype = ap_req.ticket.enc_part.etype; + +next_kvno: + krbtgt_keys = hdb_kvno2keys(r->context, r->krbtgt, krbtgt_kvno_try); + ret = hdb_enctype2key(r->context, r->krbtgt, krbtgt_keys, + ap_req.ticket.enc_part.etype, &tkey); + if (ret && krbtgt_kvno == 0 && kvno_search_tries > 0) { + kvno_search_tries--; + krbtgt_kvno_try--; + goto next_kvno; + } else if (ret) { + char *str = NULL, *p = NULL; + + /* We should implement the MIT `trace_format()' concept */ + (void) krb5_enctype_to_string(r->context, ap_req.ticket.enc_part.etype, &str); + (void) krb5_unparse_name(r->context, princ, &p); + kdc_log(r->context, config, 4, + "No server key with enctype %s found for %s", + str ? str : "<unknown enctype>", + p ? p : "<unparse_name failed>"); + free(str); + free(p); + ret = KRB5KRB_AP_ERR_BADKEYVER; + goto out; + } + + if (b->kdc_options.validate) + verify_ap_req_flags |= KRB5_VERIFY_AP_REQ_IGNORE_INVALID; + + if (r->config->warn_ticket_addresses) + verify_ap_req_flags |= KRB5_VERIFY_AP_REQ_IGNORE_ADDRS; + + ret = krb5_verify_ap_req2(r->context, + &ac, + &ap_req, + princ, + &tkey->key, + verify_ap_req_flags, + &ap_req_options, + &r->ticket, + KRB5_KU_TGS_REQ_AUTH); + if (r->ticket && r->ticket->ticket.caddr) + kdc_audit_addaddrs((kdc_request_t)r, r->ticket->ticket.caddr, "tixaddrs"); + if (r->config->warn_ticket_addresses && ret == KRB5KRB_AP_ERR_BADADDR && + r->ticket != NULL) { + kdc_audit_setkv_bool((kdc_request_t)r, "wrongaddr", TRUE); + ret = 0; + } + if (ret == KRB5KRB_AP_ERR_BAD_INTEGRITY && kvno_search_tries > 0) { + kvno_search_tries--; + krbtgt_kvno_try--; + goto next_kvno; + } + + krb5_free_principal(r->context, princ); + if(ret) { + const char *msg = krb5_get_error_message(r->context, ret); + kdc_log(r->context, config, 4, "Failed to verify AP-REQ: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + r->ticket_key = tkey; + + { + krb5_authenticator auth; + + ret = krb5_auth_con_getauthenticator(r->context, ac, &auth); + if (ret == 0) { + *csec = malloc(sizeof(**csec)); + if (*csec == NULL) { + krb5_free_authenticator(r->context, &auth); + kdc_log(r->context, config, 4, "malloc failed"); + goto out; + } + **csec = auth->ctime; + *cusec = malloc(sizeof(**cusec)); + if (*cusec == NULL) { + krb5_free_authenticator(r->context, &auth); + kdc_log(r->context, config, 4, "malloc failed"); + goto out; + } + **cusec = auth->cusec; + + ret = validate_fast_ad(r, auth->authorization_data); + krb5_free_authenticator(r->context, &auth); + if (ret) + goto out; + } + } + + ret = tgs_check_authenticator(r->context, config, ac, b, &r->ticket->ticket.key); + if (ret) { + krb5_auth_con_free(r->context, ac); + goto out; + } + + r->rk_is_subkey = 1; + + ret = krb5_auth_con_getremotesubkey(r->context, ac, &subkey); + if(ret){ + const char *msg = krb5_get_error_message(r->context, ret); + krb5_auth_con_free(r->context, ac); + kdc_log(r->context, config, 4, "Failed to get remote subkey: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + if(subkey == NULL){ + r->rk_is_subkey = 0; + + ret = krb5_auth_con_getkey(r->context, ac, &subkey); + if(ret) { + const char *msg = krb5_get_error_message(r->context, ret); + krb5_auth_con_free(r->context, ac); + kdc_log(r->context, config, 4, "Failed to get session key: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + } + if(subkey == NULL){ + krb5_auth_con_free(r->context, ac); + kdc_log(r->context, config, 4, + "Failed to get key for enc-authorization-data"); + ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; /* ? */ + goto out; + } + + krb5_free_keyblock_contents(r->context, &r->reply_key); + ret = krb5_copy_keyblock_contents(r->context, subkey, &r->reply_key); + krb5_free_keyblock(r->context, subkey); + if (ret) + goto out; + + krb5_free_keyblock_contents(r->context, &r->enc_ad_key); + if (b->enc_authorization_data) { + ret = krb5_copy_keyblock_contents(r->context, + &r->reply_key, + &r->enc_ad_key); + if (ret) + goto out; + } + + ret = validate_fast_ad(r, r->ticket->ticket.authorization_data); + if (ret) + goto out; + + + /* + * Check for FAST request + */ + + ret = _kdc_fast_unwrap_request(r, r->ticket, ac); + if (ret) + goto out; + + krb5_auth_con_free(r->context, ac); + +out: + free_AP_REQ(&ap_req); + + return ret; +} + +static krb5_error_code +build_server_referral(krb5_context context, + krb5_kdc_configuration *config, + krb5_crypto session, + krb5_const_realm referred_realm, + const PrincipalName *true_principal_name, + const PrincipalName *requested_principal, + krb5_data *outdata) +{ + PA_ServerReferralData ref; + krb5_error_code ret; + EncryptedData ed; + krb5_data data; + size_t size = 0; + + memset(&ref, 0, sizeof(ref)); + + if (referred_realm) { + ALLOC(ref.referred_realm); + if (ref.referred_realm == NULL) + goto eout; + *ref.referred_realm = strdup(referred_realm); + if (*ref.referred_realm == NULL) + goto eout; + } + if (true_principal_name) { + ALLOC(ref.true_principal_name); + if (ref.true_principal_name == NULL) + goto eout; + ret = copy_PrincipalName(true_principal_name, ref.true_principal_name); + if (ret) + goto eout; + } + if (requested_principal) { + ALLOC(ref.requested_principal_name); + if (ref.requested_principal_name == NULL) + goto eout; + ret = copy_PrincipalName(requested_principal, + ref.requested_principal_name); + if (ret) + goto eout; + } + + ASN1_MALLOC_ENCODE(PA_ServerReferralData, + data.data, data.length, + &ref, &size, ret); + free_PA_ServerReferralData(&ref); + if (ret) + return ret; + if (data.length != size) + krb5_abortx(context, "internal asn.1 encoder error"); + + ret = krb5_encrypt_EncryptedData(context, session, + KRB5_KU_PA_SERVER_REFERRAL, + data.data, data.length, + 0 /* kvno */, &ed); + free(data.data); + if (ret) + return ret; + + ASN1_MALLOC_ENCODE(EncryptedData, + outdata->data, outdata->length, + &ed, &size, ret); + free_EncryptedData(&ed); + if (ret) + return ret; + if (outdata->length != size) + krb5_abortx(context, "internal asn.1 encoder error"); + + return 0; +eout: + free_PA_ServerReferralData(&ref); + krb5_set_error_message(context, ENOMEM, "malloc: out of memory"); + return ENOMEM; +} + +/* + * This function is intended to be used when failure to find the client is + * acceptable. + */ +krb5_error_code +_kdc_db_fetch_client(krb5_context context, + krb5_kdc_configuration *config, + int flags, + krb5_principal cp, + const char *cpn, + const char *krbtgt_realm, + HDB **clientdb, + hdb_entry **client_out) +{ + krb5_error_code ret; + hdb_entry *client = NULL; + + *clientdb = NULL; + *client_out = NULL; + + ret = _kdc_db_fetch(context, config, cp, HDB_F_GET_CLIENT | flags, + NULL, clientdb, &client); + if (ret == HDB_ERR_NOT_FOUND_HERE) { + /* + * This is OK, we are just trying to find out if they have + * been disabled or deleted in the meantime; missing secrets + * are OK. + */ + } else if (ret) { + /* + * If the client belongs to the same realm as our TGS, it + * should exist in the local database. + */ + const char *msg; + + if (strcmp(krb5_principal_get_realm(context, cp), krbtgt_realm) == 0) { + if (ret == HDB_ERR_NOENTRY) + ret = KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN; + kdc_log(context, config, 4, "Client no longer in database: %s", cpn); + return ret; + } + + msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 4, "Client not found in database: %s", msg); + krb5_free_error_message(context, msg); + } else if (client->flags.invalid || !client->flags.client) { + kdc_log(context, config, 4, "Client has invalid bit set"); + _kdc_free_ent(context, *clientdb, client); + return KRB5KDC_ERR_POLICY; + } + + *client_out = client; + + return 0; +} + +static krb5_error_code +tgs_build_reply(astgs_request_t priv, + krb5_enctype krbtgt_etype, + const struct sockaddr *from_addr) +{ + krb5_context context = priv->context; + krb5_kdc_configuration *config = priv->config; + KDC_REQ_BODY *b = &priv->req.req_body; + const char *from = priv->from; + krb5_error_code ret, ret2; + krb5_principal krbtgt_out_principal = NULL; + krb5_principal user2user_princ = NULL; + char *spn = NULL, *cpn = NULL, *krbtgt_out_n = NULL; + char *user2user_name = NULL; + HDB *user2user_krbtgtdb; + hdb_entry *user2user_krbtgt = NULL; + HDB *clientdb = NULL; + HDB *serverdb = NULL; + krb5_realm ref_realm = NULL; + EncTicketPart *tgt = &priv->ticket->ticket; + const EncryptionKey *ekey; + krb5_keyblock sessionkey; + krb5_kvno kvno; + krb5_pac user2user_pac = NULL; + uint16_t rodc_id; + krb5_boolean add_ticket_sig = FALSE; + const char *tgt_realm = /* Realm of TGT issuer */ + krb5_principal_get_realm(context, priv->krbtgt->principal); + const char *our_realm = /* Realm of this KDC */ + krb5_principal_get_comp_string(context, priv->krbtgt->principal, 1); + char **capath = NULL; + size_t num_capath = 0; + AuthorizationData *auth_data = NULL; + + HDB *krbtgt_outdb; + hdb_entry *krbtgt_out = NULL; + + PrincipalName *s; + Realm r; + EncTicketPart adtkt; + char opt_str[128]; + krb5_boolean kdc_issued = FALSE; + + Key *tkey_sign; + int flags = HDB_F_FOR_TGS_REQ; + int server_flags; + + int result; + + const PA_DATA *for_user = NULL; + int for_user_idx = 0; + + memset(&sessionkey, 0, sizeof(sessionkey)); + memset(&adtkt, 0, sizeof(adtkt)); + + s = b->sname; + r = b->realm; + + /* + * The canonicalize KDC option is passed as a hint to the backend, but + * can typically be ignored. Per RFC 6806, names are not canonicalized + * in response to a TGS request (although we make an exception, see + * force-canonicalize below). + */ + if (b->kdc_options.canonicalize) + flags |= HDB_F_CANON; + + server_flags = HDB_F_GET_SERVER | HDB_F_DELAY_NEW_KEYS | flags; + if (b->kdc_options.enc_tkt_in_skey) + server_flags |= HDB_F_USER2USER_PRINCIPAL; + + if (s == NULL) { + ret = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; + _kdc_set_const_e_text(priv, "No server in request"); + goto out; + } + + _krb5_principalname2krb5_principal(context, &priv->server_princ, *s, r); + ret = krb5_unparse_name(context, priv->server_princ, &priv->sname); + if (ret) + goto out; + spn = priv->sname; + _krb5_principalname2krb5_principal(context, &priv->client_princ, + tgt->cname, tgt->crealm); + ret = krb5_unparse_name(context, priv->client_princ, &priv->cname); + if (ret) + goto out; + cpn = priv->cname; + result = unparse_flags(KDCOptions2int(b->kdc_options), + asn1_KDCOptions_units(), + opt_str, sizeof(opt_str)); + if (result > 0) + kdc_log(context, config, 4, + "TGS-REQ %s from %s for %s [%s]", + cpn, from, spn, opt_str); + else + kdc_log(context, config, 4, + "TGS-REQ %s from %s for %s", cpn, from, spn); + + /* + * Fetch server + */ + +server_lookup: + if (priv->server) + _kdc_free_ent(context, serverdb, priv->server); + priv->server = NULL; + ret = _kdc_db_fetch(context, config, priv->server_princ, + server_flags, + NULL, &serverdb, &priv->server); + priv->serverdb = serverdb; + if (ret == HDB_ERR_NOT_FOUND_HERE) { + kdc_log(context, config, 5, "target %s does not have secrets at this KDC, need to proxy", spn); + kdc_audit_addreason((kdc_request_t)priv, "Target not found here"); + goto out; + } else if (ret == HDB_ERR_WRONG_REALM) { + free(ref_realm); + ref_realm = strdup(priv->server->principal->realm); + if (ref_realm == NULL) { + ret = krb5_enomem(context); + goto out; + } + + kdc_log(context, config, 4, + "Returning a referral to realm %s for " + "server %s.", + ref_realm, spn); + krb5_free_principal(context, priv->server_princ); + priv->server_princ = NULL; + ret = krb5_make_principal(context, &priv->server_princ, r, KRB5_TGS_NAME, + ref_realm, NULL); + if (ret) + goto out; + free(priv->sname); + priv->sname = NULL; + ret = krb5_unparse_name(context, priv->server_princ, &priv->sname); + if (ret) + goto out; + spn = priv->sname; + + goto server_lookup; + } else if (ret) { + const char *new_rlm, *msg; + Realm req_rlm; + krb5_realm *realms; + + priv->error_code = ret; /* advise policy plugin of failure reason */ + ret2 = _kdc_referral_policy(priv); + if (ret2 == 0) { + krb5_xfree(priv->sname); + priv->sname = NULL; + ret = krb5_unparse_name(context, priv->server_princ, &priv->sname); + if (ret) + goto out; + goto server_lookup; + } else if (ret2 != KRB5_PLUGIN_NO_HANDLE) { + ret = ret2; + } else if ((req_rlm = get_krbtgt_realm(&priv->server_princ->name)) != NULL) { + if (capath == NULL) { + /* With referalls, hierarchical capaths are always enabled */ + ret2 = _krb5_find_capath(context, tgt->crealm, our_realm, + req_rlm, TRUE, &capath, &num_capath); + if (ret2) { + ret = ret2; + kdc_audit_addreason((kdc_request_t)priv, + "No trusted path from client realm to ours"); + goto out; + } + } + new_rlm = num_capath > 0 ? capath[--num_capath] : NULL; + if (new_rlm) { + kdc_log(context, config, 5, "krbtgt from %s via %s for " + "realm %s not found, trying %s", tgt->crealm, + our_realm, req_rlm, new_rlm); + + free(ref_realm); + ref_realm = strdup(new_rlm); + if (ref_realm == NULL) { + ret = krb5_enomem(context); + goto out; + } + + krb5_free_principal(context, priv->server_princ); + priv->server_princ = NULL; + krb5_make_principal(context, &priv->server_princ, r, + KRB5_TGS_NAME, ref_realm, NULL); + free(priv->sname); + priv->sname = NULL; + ret = krb5_unparse_name(context, priv->server_princ, &priv->sname); + if (ret) + goto out; + spn = priv->sname; + goto server_lookup; + } + } else if (need_referral(context, config, &b->kdc_options, priv->server_princ, &realms)) { + if (strcmp(realms[0], priv->server_princ->realm) != 0) { + kdc_log(context, config, 4, + "Returning a referral to realm %s for " + "server %s that was not found", + realms[0], spn); + krb5_free_principal(context, priv->server_princ); + priv->server_princ = NULL; + krb5_make_principal(context, &priv->server_princ, r, KRB5_TGS_NAME, + realms[0], NULL); + free(priv->sname); + priv->sname = NULL; + ret = krb5_unparse_name(context, priv->server_princ, &priv->sname); + if (ret) { + krb5_free_host_realm(context, realms); + goto out; + } + spn = priv->sname; + + free(ref_realm); + ref_realm = strdup(realms[0]); + + krb5_free_host_realm(context, realms); + goto server_lookup; + } + krb5_free_host_realm(context, realms); + } + msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 3, + "Server not found in database: %s: %s", spn, msg); + krb5_free_error_message(context, msg); + if (ret == HDB_ERR_NOENTRY) + ret = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; + kdc_audit_addreason((kdc_request_t)priv, + "Service principal unknown"); + goto out; + } + + /* + * Now refetch the primary krbtgt, and get the current kvno (the + * sign check may have been on an old kvno, and the server may + * have been an incoming trust) + */ + + ret = krb5_make_principal(context, + &krbtgt_out_principal, + our_realm, + KRB5_TGS_NAME, + our_realm, + NULL); + if (ret) { + kdc_log(context, config, 4, + "Failed to make krbtgt principal name object for " + "authz-data signatures"); + goto out; + } + ret = krb5_unparse_name(context, krbtgt_out_principal, &krbtgt_out_n); + if (ret) { + kdc_log(context, config, 4, + "Failed to make krbtgt principal name object for " + "authz-data signatures"); + goto out; + } + + ret = _kdc_db_fetch(context, config, krbtgt_out_principal, + HDB_F_GET_KRBTGT, NULL, &krbtgt_outdb, &krbtgt_out); + if (ret) { + char *ktpn = NULL; + ret = krb5_unparse_name(context, priv->krbtgt->principal, &ktpn); + kdc_log(context, config, 4, + "No such principal %s (needed for authz-data signature keys) " + "while processing TGS-REQ for service %s with krbtgt %s", + krbtgt_out_n, spn, (ret == 0) ? ktpn : "<unknown>"); + free(ktpn); + ret = KRB5KRB_AP_ERR_NOT_US; + goto out; + } + + /* + * Select enctype, return key and kvno. + */ + + { + krb5_enctype etype; + + if(b->kdc_options.enc_tkt_in_skey) { + Ticket *t; + krb5_principal p; + Key *uukey; + krb5uint32 second_kvno = 0; + krb5uint32 *kvno_ptr = NULL; + size_t i; + HDB *user2user_db; + hdb_entry *user2user_client = NULL; + krb5_boolean user2user_kdc_issued = FALSE; + char *tpn; + + if(b->additional_tickets == NULL || + b->additional_tickets->len == 0){ + ret = KRB5KDC_ERR_BADOPTION; /* ? */ + kdc_log(context, config, 4, + "No second ticket present in user-to-user request"); + kdc_audit_addreason((kdc_request_t)priv, + "No second ticket present in user-to-user request"); + goto out; + } + t = &b->additional_tickets->val[0]; + if(!krb5_principalname_is_krbtgt(context, &t->sname)){ + /* + * Note: this check is not to be depended upon for + * security. Nothing prevents a client modifying the sname, as + * it is located in the unencrypted part of the ticket. + */ + + kdc_log(context, config, 4, + "Additional ticket is not a ticket-granting ticket"); + kdc_audit_addreason((kdc_request_t)priv, + "Additional ticket is not a ticket-granting ticket"); + ret = KRB5KDC_ERR_POLICY; + goto out; + } + ret = _krb5_principalname2krb5_principal(context, &p, t->sname, t->realm); + if (ret) + goto out; + + ret = krb5_unparse_name(context, p, &tpn); + if (ret) + goto out; + if(t->enc_part.kvno){ + second_kvno = *t->enc_part.kvno; + kvno_ptr = &second_kvno; + } + ret = _kdc_db_fetch(context, config, p, + HDB_F_GET_KRBTGT, kvno_ptr, + &user2user_krbtgtdb, &user2user_krbtgt); + krb5_free_principal(context, p); + if(ret){ + if (ret == HDB_ERR_NOENTRY) + ret = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; + kdc_audit_addreason((kdc_request_t)priv, + "User-to-user service principal (TGS) unknown"); + krb5_xfree(tpn); + goto out; + } + ret = hdb_enctype2key(context, user2user_krbtgt, NULL, + t->enc_part.etype, &uukey); + if(ret){ + ret = KRB5KDC_ERR_ETYPE_NOSUPP; /* XXX */ + kdc_audit_addreason((kdc_request_t)priv, + "User-to-user enctype not supported"); + krb5_xfree(tpn); + goto out; + } + ret = krb5_decrypt_ticket(context, t, &uukey->key, &adtkt, 0); + if(ret) { + kdc_audit_addreason((kdc_request_t)priv, + "User-to-user TGT decrypt failure"); + krb5_xfree(tpn); + goto out; + } + + ret = _kdc_verify_flags(context, config, &adtkt, tpn); + if (ret) { + kdc_audit_addreason((kdc_request_t)priv, + "User-to-user TGT expired or invalid"); + krb5_xfree(tpn); + goto out; + } + krb5_xfree(tpn); + + /* Fetch the name from the TGT. */ + ret = _krb5_principalname2krb5_principal(context, &user2user_princ, + adtkt.cname, adtkt.crealm); + if (ret) + goto out; + + ret = krb5_unparse_name(context, user2user_princ, &user2user_name); + if (ret) + goto out; + + /* + * Look up the name given in the TGT in the database. The user + * claims to have a ticket-granting-ticket to our KDC, so we should + * fail hard if we can't find the user - otherwise we can't do + * proper checks. + */ + ret = _kdc_db_fetch(context, config, user2user_princ, + HDB_F_GET_CLIENT | flags, + NULL, &user2user_db, &user2user_client); + if (ret == HDB_ERR_NOENTRY) + ret = KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN; + if (ret) + goto out; + + /* + * The account is present in the database, now check the + * account flags. + * + * We check this as a client (because the purpose of + * user2user is that the server flag is not set, because + * the long-term key is not strong, but this does mean + * that a client with an expired password can't get accept + * a user2user ticket. + */ + ret = kdc_check_flags(priv, + FALSE, + user2user_client, + NULL); + if (ret) { + _kdc_free_ent(context, user2user_db, user2user_client); + goto out; + } + + /* + * Also check that the account is the same one specified in the + * request. + */ + ret = _kdc_check_client_matches_target_service(context, + config, + serverdb, + priv->server, + user2user_client, + user2user_princ); + if (ret) { + _kdc_free_ent(context, user2user_db, user2user_client); + goto out; + } + + /* Verify the PAC of the TGT. */ + ret = _kdc_check_pac(priv, user2user_princ, NULL, + user2user_client, user2user_krbtgt, user2user_krbtgt, user2user_krbtgt, + &uukey->key, &priv->ticket_key->key, &adtkt, + &user2user_kdc_issued, &user2user_pac, NULL, NULL); + _kdc_free_ent(context, user2user_db, user2user_client); + if (ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 0, + "Verify PAC failed for %s (%s) from %s with %s", + spn, user2user_name, from, msg); + krb5_free_error_message(context, msg); + goto out; + } + + if ((config->require_pac && !user2user_pac) + || (user2user_pac && !user2user_kdc_issued)) + { + ret = KRB5KDC_ERR_BADOPTION; + kdc_log(context, config, 0, + "Ticket not signed with PAC; user-to-user failed (%s).", + user2user_pac ? "Ticket unsigned" : "No PAC"); + goto out; + } + + ekey = &adtkt.key; + for(i = 0; i < b->etype.len; i++) + if (b->etype.val[i] == adtkt.key.keytype) + break; + if(i == b->etype.len) { + kdc_log(context, config, 4, + "Addition ticket has no matching etypes"); + krb5_clear_error_message(context); + ret = KRB5KDC_ERR_ETYPE_NOSUPP; + kdc_audit_addreason((kdc_request_t)priv, + "No matching enctypes for 2nd ticket"); + goto out; + } + etype = b->etype.val[i]; + kvno = 0; + } else { + Key *skey; + + ret = _kdc_find_session_etype(priv, b->etype.val, b->etype.len, + priv->server, &etype); + if(ret) { + kdc_log(context, config, 4, + "Server (%s) has no support for etypes", spn); + kdc_audit_addreason((kdc_request_t)priv, + "Enctype not supported"); + goto out; + } + ret = _kdc_get_preferred_key(context, config, priv->server, spn, + NULL, &skey); + if(ret) { + kdc_log(context, config, 4, + "Server (%s) has no supported etypes", spn); + kdc_audit_addreason((kdc_request_t)priv, + "Enctype not supported"); + goto out; + } + ekey = &skey->key; + kvno = priv->server->kvno; + } + + ret = krb5_generate_random_keyblock(context, etype, &sessionkey); + if (ret) + goto out; + } + + /* + * Check that service is in the same realm as the krbtgt. If it's + * not the same, it's someone that is using a uni-directional trust + * backward. + */ + + /* + * The first realm is the realm of the service, the second is + * krbtgt/<this>/@REALM component of the krbtgt DN the request was + * encrypted to. The redirection via the krbtgt_out entry allows + * the DB to possibly correct the case of the realm (Samba4 does + * this) before the strcmp() + */ + if (strcmp(krb5_principal_get_realm(context, priv->server->principal), + krb5_principal_get_realm(context, krbtgt_out->principal)) != 0) { + char *ktpn; + ret = krb5_unparse_name(context, krbtgt_out->principal, &ktpn); + kdc_log(context, config, 4, + "Request with wrong krbtgt: %s", + (ret == 0) ? ktpn : "<unknown>"); + if(ret == 0) + free(ktpn); + ret = KRB5KRB_AP_ERR_NOT_US; + kdc_audit_addreason((kdc_request_t)priv, "Request with wrong TGT"); + goto out; + } + + ret = _kdc_get_preferred_key(context, config, krbtgt_out, krbtgt_out_n, + NULL, &tkey_sign); + if (ret) { + kdc_log(context, config, 4, + "Failed to find key for krbtgt PAC signature"); + kdc_audit_addreason((kdc_request_t)priv, + "Failed to find key for krbtgt PAC signature"); + goto out; + } + ret = hdb_enctype2key(context, krbtgt_out, NULL, + tkey_sign->key.keytype, &tkey_sign); + if(ret) { + kdc_log(context, config, 4, + "Failed to find key for krbtgt PAC signature"); + kdc_audit_addreason((kdc_request_t)priv, + "Failed to find key for krbtgt PAC signature"); + goto out; + } + + if (_kdc_synthetic_princ_used_p(context, priv->ticket)) + flags |= HDB_F_SYNTHETIC_OK; + + ret = _kdc_db_fetch_client(context, config, flags, priv->client_princ, + cpn, our_realm, &clientdb, &priv->client); + if (ret) + goto out; + /* flags &= ~HDB_F_SYNTHETIC_OK; */ /* `flags' is not used again below */ + priv->clientdb = clientdb; + + /* Validate armor TGT before potentially including device claims */ + if (priv->armor_ticket) { + ret = _kdc_fast_check_armor_pac(priv, HDB_F_FOR_TGS_REQ); + if (ret) + goto out; + } + + ret = _kdc_check_pac(priv, priv->client_princ, NULL, + priv->client, priv->server, + priv->krbtgt, priv->krbtgt, + &priv->ticket_key->key, &priv->ticket_key->key, tgt, + &kdc_issued, &priv->pac, &priv->canon_client_princ, + &priv->pac_attributes); + if (ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_audit_addreason((kdc_request_t)priv, "PAC check failed"); + kdc_log(context, config, 4, + "Verify PAC failed for %s (%s) from %s with %s", + spn, cpn, from, msg); + krb5_free_error_message(context, msg); + goto out; + } + + /* + * Process request + */ + + /* + * Services for User: protocol transition and constrained delegation + */ + + if (priv->client != NULL && + (for_user = _kdc_find_padata(&priv->req, + &for_user_idx, + KRB5_PADATA_FOR_USER)) != NULL) + { + /* Process an S4U2Self request. */ + ret = _kdc_validate_protocol_transition(priv, for_user); + if (ret) + goto out; + } else if (priv->client != NULL + && b->additional_tickets != NULL + && b->additional_tickets->len != 0 + && b->kdc_options.cname_in_addl_tkt + && b->kdc_options.enc_tkt_in_skey == 0) + { + /* Process an S4U2Proxy request. */ + ret = _kdc_validate_constrained_delegation(priv); + if (ret) + goto out; + } else if (priv->pac != NULL) { + ret = _kdc_pac_update(priv, priv->client_princ, NULL, NULL, + priv->client, priv->server, priv->krbtgt, + &priv->pac); + if (ret == KRB5_PLUGIN_NO_HANDLE) { + ret = 0; + } + if (ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_audit_addreason((kdc_request_t)priv, "PAC update failed"); + kdc_log(context, config, 4, + "Update PAC failed for %s (%s) from %s with %s", + spn, cpn, from, msg); + krb5_free_error_message(context, msg); + goto out; + } + + if (priv->pac == NULL) { + /* the plugin may indicate no PAC should be generated */ + priv->pac_attributes = 0; + } + } + + if (b->enc_authorization_data) { + unsigned auth_data_usage; + krb5_crypto crypto; + krb5_data ad; + + if (priv->rk_is_subkey != 0) { + auth_data_usage = KRB5_KU_TGS_REQ_AUTH_DAT_SUBKEY; + } else { + auth_data_usage = KRB5_KU_TGS_REQ_AUTH_DAT_SESSION; + } + + ret = krb5_crypto_init(context, &priv->enc_ad_key, 0, &crypto); + if (ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_audit_addreason((kdc_request_t)priv, + "krb5_crypto_init() failed for " + "enc_authorization_data"); + kdc_log(context, config, 4, "krb5_crypto_init failed: %s", msg); + krb5_free_error_message(context, msg); + goto out; + } + ret = krb5_decrypt_EncryptedData(context, + crypto, + auth_data_usage, + b->enc_authorization_data, + &ad); + krb5_crypto_destroy(context, crypto); + if(ret){ + kdc_audit_addreason((kdc_request_t)priv, + "Failed to decrypt enc-authorization-data"); + kdc_log(context, config, 4, + "Failed to decrypt enc-authorization-data"); + ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; /* ? */ + goto out; + } + ALLOC(auth_data); + if (auth_data == NULL) { + krb5_data_free(&ad); + ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; /* ? */ + goto out; + } + ret = decode_AuthorizationData(ad.data, ad.length, auth_data, NULL); + krb5_data_free(&ad); + if(ret){ + free(auth_data); + auth_data = NULL; + kdc_audit_addreason((kdc_request_t)priv, + "Failed to decode authorization data"); + kdc_log(context, config, 4, "Failed to decode authorization data"); + ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; /* ? */ + goto out; + } + } + + /* + * Check flags + */ + + ret = kdc_check_flags(priv, FALSE, priv->client, priv->server); + if(ret) + goto out; + + if((b->kdc_options.validate || b->kdc_options.renew) && + !krb5_principal_compare(context, + priv->krbtgt->principal, + priv->server->principal)){ + kdc_audit_addreason((kdc_request_t)priv, "Inconsistent request"); + kdc_log(context, config, 4, "Inconsistent request."); + ret = KRB5KDC_ERR_SERVER_NOMATCH; + goto out; + } + + /* check for valid set of addresses */ + if (!_kdc_check_addresses(priv, tgt->caddr, from_addr)) { + if (config->check_ticket_addresses) { + ret = KRB5KRB_AP_ERR_BADADDR; + kdc_audit_setkv_bool((kdc_request_t)priv, "wrongaddr", TRUE); + kdc_log(context, config, 4, "Request from wrong address"); + kdc_audit_addreason((kdc_request_t)priv, "Request from wrong address"); + goto out; + } else if (config->warn_ticket_addresses) { + kdc_audit_setkv_bool((kdc_request_t)priv, "wrongaddr", TRUE); + } + } + + /* check local and per-principal anonymous ticket issuance policy */ + if (is_anon_tgs_request_p(b, tgt)) { + ret = _kdc_check_anon_policy(priv); + if (ret) + goto out; + } + + /* + * If this is an referral, add server referral data to the + * auth_data reply . + */ + if (ref_realm) { + PA_DATA pa; + krb5_crypto crypto; + + kdc_log(context, config, 3, + "Adding server referral to %s", ref_realm); + + ret = krb5_crypto_init(context, &sessionkey, 0, &crypto); + if (ret) + goto out; + + ret = build_server_referral(context, config, crypto, ref_realm, + NULL, s, &pa.padata_value); + krb5_crypto_destroy(context, crypto); + if (ret) { + kdc_audit_addreason((kdc_request_t)priv, "Referral build failed"); + kdc_log(context, config, 4, + "Failed building server referral"); + goto out; + } + pa.padata_type = KRB5_PADATA_SERVER_REFERRAL; + + ret = add_METHOD_DATA(priv->rep.padata, &pa); + krb5_data_free(&pa.padata_value); + if (ret) { + kdc_log(context, config, 4, + "Add server referral METHOD-DATA failed"); + goto out; + } + } + + /* + * Only add ticket signature if the requested server is not krbtgt, and + * either the header server is krbtgt or, in the case of renewal/validation + * if it was signed with PAC ticket signature and we verified it. + * Currently Heimdal only allows renewal of krbtgt anyway but that might + * change one day (see issue #763) so make sure to check for it. + */ + + if (kdc_issued && + !krb5_principal_is_krbtgt(context, priv->server->principal)) { + + add_ticket_sig = TRUE; + } + + /* + * Active-Directory implementations use the high part of the kvno as the + * read-only-dc identifier, we need to embed it in the PAC KDC signatures. + */ + + rodc_id = krbtgt_out->kvno >> 16; + + /* + * + */ + + ret = tgs_make_reply(priv, + tgt, + ekey, + &tkey_sign->key, + &sessionkey, + kvno, + auth_data, + tgt_realm, + rodc_id, + add_ticket_sig); + +out: + free(user2user_name); + free(krbtgt_out_n); + _krb5_free_capath(context, capath); + + krb5_free_keyblock_contents(context, &sessionkey); + if(krbtgt_out) + _kdc_free_ent(context, krbtgt_outdb, krbtgt_out); + if(user2user_krbtgt) + _kdc_free_ent(context, user2user_krbtgtdb, user2user_krbtgt); + + krb5_free_principal(context, user2user_princ); + krb5_free_principal(context, krbtgt_out_principal); + free(ref_realm); + + if (auth_data) { + free_AuthorizationData(auth_data); + free(auth_data); + } + + free_EncTicketPart(&adtkt); + + krb5_pac_free(context, user2user_pac); + + return ret; +} + +/* + * + */ + +krb5_error_code +_kdc_tgs_rep(astgs_request_t r) +{ + krb5_kdc_configuration *config = r->config; + KDC_REQ *req = &r->req; + krb5_data *data = r->reply; + const char *from = r->from; + struct sockaddr *from_addr = r->addr; + int datagram_reply = r->datagram_reply; + krb5_error_code ret; + int i = 0; + const PA_DATA *tgs_req, *pa; + krb5_enctype krbtgt_etype = ETYPE_NULL; + + time_t *csec = NULL; + int *cusec = NULL; + + r->e_text = NULL; + + if(req->padata == NULL){ + ret = KRB5KDC_ERR_PREAUTH_REQUIRED; /* XXX ??? */ + kdc_log(r->context, config, 4, + "TGS-REQ from %s without PA-DATA", from); + goto out; + } + + i = 0; + pa = _kdc_find_padata(&r->req, &i, KRB5_PADATA_FX_FAST_ARMOR); + if (pa) { + kdc_log(r->context, r->config, 10, "Found TGS-REQ FAST armor inside TGS-REQ pa-data"); + ret = KRB5KRB_ERR_GENERIC; + goto out; + } + + i = 0; + tgs_req = _kdc_find_padata(req, &i, KRB5_PADATA_TGS_REQ); + if(tgs_req == NULL){ + ret = KRB5KDC_ERR_PADATA_TYPE_NOSUPP; + + kdc_log(r->context, config, 4, + "TGS-REQ from %s without PA-TGS-REQ", from); + goto out; + } + ret = tgs_parse_request(r, tgs_req, + &krbtgt_etype, + from, from_addr, + &csec, &cusec); + if (ret == HDB_ERR_NOT_FOUND_HERE) { + /* kdc_log() is called in tgs_parse_request() */ + goto out; + } + if (ret) { + kdc_log(r->context, config, 4, + "Failed parsing TGS-REQ from %s", from); + goto out; + } + + ret = _kdc_fast_strengthen_reply_key(r); + if (ret) + goto out; + + ALLOC(r->rep.padata); + if (r->rep.padata == NULL) { + ret = ENOMEM; + krb5_set_error_message(r->context, ret, N_("malloc: out of memory", "")); + goto out; + } + + ret = tgs_build_reply(r, + krbtgt_etype, + from_addr); + if (ret) { + kdc_log(r->context, config, 4, + "Failed building TGS-REP to %s", from); + goto out; + } + + /* */ + if (datagram_reply && data->length > config->max_datagram_reply_length) { + krb5_data_free(data); + ret = KRB5KRB_ERR_RESPONSE_TOO_BIG; + _kdc_set_const_e_text(r, "Reply packet too large"); + } + +out: + if (ret) { + /* Overwrite ‘error_code’ only if we have an actual error. */ + r->error_code = ret; + } + { + krb5_error_code ret2 = _kdc_audit_request(r); + if (ret2) { + krb5_data_free(data); + ret = ret2; + } + } + + if(ret && ret != HDB_ERR_NOT_FOUND_HERE && data->data == NULL){ + METHOD_DATA error_method = { 0, NULL }; + + kdc_log(r->context, config, 5, "tgs-req: sending error: %d to client", ret); + ret = _kdc_fast_mk_error(r, + &error_method, + r->armor_crypto, + &req->req_body, + r->error_code ? r->error_code : ret, + r->client_princ ? r->client_princ :(r->ticket != NULL ? r->ticket->client : NULL), + r->server_princ ? r->server_princ :(r->ticket != NULL ? r->ticket->server : NULL), + csec, cusec, + data); + free_METHOD_DATA(&error_method); + } + free(csec); + free(cusec); + + free_TGS_REP(&r->rep); + free_TransitedEncoding(&r->et.transited); + free(r->et.starttime); + free(r->et.renew_till); + if(r->et.authorization_data) { + free_AuthorizationData(r->et.authorization_data); + free(r->et.authorization_data); + } + free_LastReq(&r->ek.last_req); + if (r->et.key.keyvalue.data) { + memset_s(r->et.key.keyvalue.data, 0, r->et.key.keyvalue.length, + r->et.key.keyvalue.length); + } + free_EncryptionKey(&r->et.key); + + if (r->canon_client_princ) { + krb5_free_principal(r->context, r->canon_client_princ); + r->canon_client_princ = NULL; + } + if (r->armor_crypto) { + krb5_crypto_destroy(r->context, r->armor_crypto); + r->armor_crypto = NULL; + } + if (r->armor_ticket) + krb5_free_ticket(r->context, r->armor_ticket); + if (r->armor_server) + _kdc_free_ent(r->context, r->armor_serverdb, r->armor_server); + if (r->armor_client) + _kdc_free_ent(r->context, + r->armor_clientdb, + r->armor_client); + if (r->armor_pac) + krb5_pac_free(r->context, r->armor_pac); + krb5_free_keyblock_contents(r->context, &r->reply_key); + krb5_free_keyblock_contents(r->context, &r->enc_ad_key); + krb5_free_keyblock_contents(r->context, &r->strengthen_key); + + if (r->ticket) + krb5_free_ticket(r->context, r->ticket); + if (r->krbtgt) + _kdc_free_ent(r->context, r->krbtgtdb, r->krbtgt); + + if (r->client) + _kdc_free_ent(r->context, r->clientdb, r->client); + krb5_free_principal(r->context, r->client_princ); + if (r->server) + _kdc_free_ent(r->context, r->serverdb, r->server); + krb5_free_principal(r->context, r->server_princ); + _kdc_free_fast_state(&r->fast); + krb5_pac_free(r->context, r->pac); + + return ret; +} diff --git a/third_party/heimdal/kdc/kstash-version.rc b/third_party/heimdal/kdc/kstash-version.rc new file mode 100644 index 0000000..c3d2214 --- /dev/null +++ b/third_party/heimdal/kdc/kstash-version.rc @@ -0,0 +1,36 @@ +/*********************************************************************** + * Copyright (c) 2010, Secure Endpoints Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - 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. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 + * COPYRIGHT HOLDER 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 RC_FILE_TYPE VFT_APP +#define RC_FILE_DESC_0409 "KDC Master Password Stash Tool" +#define RC_FILE_ORIG_0409 "kstash.exe" + +#include "../windows/version.rc" diff --git a/third_party/heimdal/kdc/kstash.8 b/third_party/heimdal/kdc/kstash.8 new file mode 100644 index 0000000..615132b --- /dev/null +++ b/third_party/heimdal/kdc/kstash.8 @@ -0,0 +1,92 @@ +.\" Copyright (c) 1997 - 2004 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. +.\" +.\" $Id$ +.\" +.Dd April 10, 2007 +.Dt KSTASH 8 +.Os HEIMDAL +.Sh NAME +.Nm kstash +.Nd "store the KDC master password in a file" +.Sh SYNOPSIS +.Nm +.Bk -words +.Oo Fl e Ar string \*(Ba Xo +.Fl Fl enctype= Ns Ar string +.Xc +.Oc +.Oo Fl k Ar file \*(Ba Xo +.Fl Fl key-file= Ns Ar file +.Xc +.Oc +.Op Fl Fl convert-file +.Op Fl Fl random-key +.Op Fl Fl master-key-fd= Ns Ar fd +.Op Fl Fl random-key +.Op Fl h | Fl Fl help +.Op Fl Fl version +.Ek +.Sh DESCRIPTION +.Nm +reads the Kerberos master key and stores it in a file that will be +used by the KDC. +.Pp +Supported options: +.Bl -tag -width Ds +.It Fl e Ar string , Fl Fl enctype= Ns Ar string +the encryption type to use, defaults to DES3-CBC-SHA1. +.It Fl k Ar file , Fl Fl key-file= Ns Ar file +the name of the master key file. +.It Fl Fl convert-file +don't ask for a new master key, just read an old master key file, and +write it back in the new keyfile format. +.It Fl Fl random-key +generate a random master key. +.It Fl Fl master-key-fd= Ns Ar fd +filedescriptor to read passphrase from, if not specified the +passphrase will be read from the terminal. +.El +.\".Sh ENVIRONMENT +.Sh FILES +.Pa /var/heimdal/m-key +is the default keyfile if no other keyfile is specified. +The format of a Heimdal master key is the same as a keytab, so +.Nm ktutil +list can be used to list the content of the file. +.\".Sh EXAMPLES +.\".Sh DIAGNOSTICS +.Sh SEE ALSO +.Xr kdc 8 +.\".Sh STANDARDS +.\".Sh HISTORY +.\".Sh AUTHORS +.\".Sh BUGS diff --git a/third_party/heimdal/kdc/kstash.c b/third_party/heimdal/kdc/kstash.c new file mode 100644 index 0000000..6ec1a54 --- /dev/null +++ b/third_party/heimdal/kdc/kstash.c @@ -0,0 +1,189 @@ +/* + * Copyright (c) 1997-2004 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. + */ + +#include "headers.h" + +krb5_context context; + +static char *keyfile; +static int convert_flag; +static int help_flag; +static int version_flag; + +static int master_key_fd = -1; +static int random_key_flag; + +static const char *enctype_str = "des3-cbc-sha1"; + +static struct getargs args[] = { + { "enctype", 'e', arg_string, rk_UNCONST(&enctype_str), "encryption type", + NULL }, + { "key-file", 'k', arg_string, &keyfile, "master key file", "file" }, + { "convert-file", 0, arg_flag, &convert_flag, + "just convert keyfile to new format", NULL }, + { "master-key-fd", 0, arg_integer, &master_key_fd, + "filedescriptor to read passphrase from", "fd" }, + { "random-key", 0, arg_flag, &random_key_flag, + "generate a random master key", NULL }, + { "help", 'h', arg_flag, &help_flag, NULL, NULL }, + { "version", 0, arg_flag, &version_flag, NULL, NULL } +}; + +int num_args = sizeof(args) / sizeof(args[0]); + +int +main(int argc, char **argv) +{ + char buf[1024+1]; + krb5_error_code ret; + int aret; + + krb5_enctype enctype; + + hdb_master_key mkey; + + krb5_program_setup(&context, argc, argv, args, num_args, NULL); + + if(help_flag) + krb5_std_usage(0, args, num_args); + if(version_flag){ + print_version(NULL); + exit(0); + } + + if (master_key_fd != -1 && random_key_flag) + krb5_errx(context, 1, "random-key and master-key-fd " + "is mutual exclusive"); + + if (keyfile == NULL) { + aret = asprintf(&keyfile, "%s/m-key", hdb_db_dir(context)); + if (aret == -1) + krb5_errx(context, 1, "out of memory"); + } + + ret = krb5_string_to_enctype(context, enctype_str, &enctype); + if(ret) + krb5_err(context, 1, ret, "krb5_string_to_enctype"); + + ret = hdb_read_master_key(context, keyfile, &mkey); + if(ret && ret != ENOENT) + krb5_err(context, 1, ret, "reading master key from %s", keyfile); + + if (convert_flag) { + if (ret) + krb5_err(context, 1, ret, "reading master key from %s", keyfile); + } else { + krb5_keyblock key; + krb5_salt salt; + salt.salttype = KRB5_PW_SALT; + /* XXX better value? */ + salt.saltvalue.data = NULL; + salt.saltvalue.length = 0; + if (random_key_flag) { + ret = krb5_generate_random_keyblock(context, enctype, &key); + if (ret) + krb5_err(context, 1, ret, "krb5_generate_random_keyblock"); + + } else { + if(master_key_fd != -1) { + ssize_t n; + n = read(master_key_fd, buf, sizeof(buf)-1); + if(n <= 0) + krb5_err(context, 1, errno, "failed to read passphrase"); + buf[n] = '\0'; + buf[strcspn(buf, "\r\n")] = '\0'; + + } else { + if(UI_UTIL_read_pw_string(buf, sizeof(buf), "Master key: ", + UI_UTIL_FLAG_VERIFY)) + exit(1); + } + krb5_string_to_key_salt(context, enctype, buf, salt, &key); + } + ret = hdb_add_master_key(context, &key, &mkey); + if (ret) + krb5_err(context, 1, ret, "hdb_add_master_key"); + + krb5_free_keyblock_contents(context, &key); + + } + + { + char *new = NULL, *old = NULL; + + aret = asprintf(&old, "%s.old", keyfile); + if (aret == -1) { + old = NULL; + ret = ENOMEM; + goto out; + } + aret = asprintf(&new, "%s.new", keyfile); + if (aret == -1) { + new = NULL; + ret = ENOMEM; + goto out; + } + if(unlink(new) < 0 && errno != ENOENT) { + ret = errno; + goto out; + } + krb5_warnx(context, "writing key to `%s'", keyfile); + ret = hdb_write_master_key(context, new, mkey); + if(ret) + unlink(new); + else { +#ifndef NO_POSIX_LINKS + unlink(old); + if(link(keyfile, old) < 0 && errno != ENOENT) { + ret = errno; + unlink(new); + } else { +#endif + if(rename(new, keyfile) < 0) { + ret = errno; + } +#ifndef NO_POSIX_LINKS + } +#endif + } + out: + free(old); + free(new); + if(ret) + krb5_warn(context, errno, "writing master key file"); + } + + hdb_free_master_key(context, mkey); + + exit(ret != 0); +} diff --git a/third_party/heimdal/kdc/kx509.c b/third_party/heimdal/kdc/kx509.c new file mode 100644 index 0000000..6efd94e --- /dev/null +++ b/third_party/heimdal/kdc/kx509.c @@ -0,0 +1,1080 @@ +/* + * Copyright (c) 2006 - 2019 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. + */ + +#include "kdc_locl.h" +#include <hex.h> +#include <rfc2459_asn1.h> +#include <hx509.h> +#include <hx509_err.h> +#include <kx509_err.h> + +#include <stdarg.h> + +/* + * This file implements the kx509 service. + * + * The protocol, its shortcomings, and its future are described in + * lib/krb5/hx509.c. See also lib/asn1/kx509.asn1. + * + * The service handles requests, decides whether to issue a certificate, and + * does so by populating a "template" to generate a TBSCertificate and signing + * it with a configured CA issuer certificate and private key. See ca.c for + * details. + * + * A "template" is a Certificate that has ${variable} references in its + * subjectName, and may have EKUs. + * + * Some SANs may be included in issued certificates. See below. + * + * Besides future protocol improvements described in lib/krb5/hx509.c, here is + * a list of KDC functionality we'd like to add: + * + * - support templates as strings (rather than filenames) in configuration? + * - lookup an hx509 template for the client principal in its HDB entry? + * - lookup subjectName, SANs for a principal in its HDB entry + * - lookup a host-based client principal's HDB entry and add its canonical + * name / aliases as dNSName SANs + * (this would have to be if requested by the client, perhaps; see + * commentary about the protocol in lib/krb5/kx509.c) + * - add code to build a template on the fly + * + * (just SANs, with empty subjectName? + * or + * CN=component0,CN=component1,..,CN=componentN,DC=<from-REALM> + * and set KU and EKUs) + * + * Processing begins in _kdc_do_kx509(). + * + * The sequence of events in _kdc_do_kx509() is: + * + * - parse outer request + * - authenticate request + * - extract CSR and AP-REQ Authenticator authz-data elements + * - characterize request as one of + * - default client cert req (no cert exts requested, client user princ) + * - default server cert req (no cert exts requested, client service princ) + * - client cert req (cert exts requested denoting client use) + * - server cert req (cert exts requested denoting server use) + * - mixed cert req (cert exts requested denoting client and server use) + * - authorize request based only on the request's details + * - there is a default authorizer, and a plugin authorizer + * - get configuration sub-tree corresponding to the request as characterized + * - missing configuration sub-tree -> reject (we have multiple ways to + * express "no") + * - get common config params from that sub-tree + * - set TBS template and details from CSR and such + * - issue certificate by signing TBS + */ + +#ifdef KX509 + +static const unsigned char version_2_0[4] = {0 , 0, 2, 0}; + +/* + * Taste the request to see if it's a kx509 request. + */ +krb5_error_code +_kdc_try_kx509_request(kx509_req_context r) +{ + const unsigned char *p = (const void *)(uintptr_t)r->request.data; + size_t len = r->request.length; + size_t sz; + + if (len < sizeof(version_2_0)) + return -1; + if (memcmp(version_2_0, p, sizeof(version_2_0)) != 0) + return -1; + p += sizeof(version_2_0); + len -= sizeof(version_2_0); + if (len == 0) + return -1; + memset(&r->req, 0, sizeof(r->req)); + return decode_Kx509Request(p, len, &r->req, &sz); +} + +static krb5_boolean +get_bool_param(krb5_context context, + krb5_boolean def, + const char *crealm, + const char *name) +{ + krb5_boolean global_default; + + global_default = krb5_config_get_bool_default(context, NULL, def, "kdc", + name, NULL); + if (!crealm) + return global_default; + return krb5_config_get_bool_default(context, NULL, global_default, + "kdc", "realms", crealm, name, NULL); +} + +/* + * Verify the HMAC in the request. + */ +static krb5_error_code +verify_req_hash(krb5_context context, + const Kx509Request *req, + krb5_keyblock *key) +{ + unsigned char digest[SHA_DIGEST_LENGTH]; + HMAC_CTX ctx; + + if (req->pk_hash.length != sizeof(digest)) { + krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED, + "pk-hash has wrong length: %lu", + (unsigned long)req->pk_hash.length); + return KRB5KDC_ERR_PREAUTH_FAILED; + } + + HMAC_CTX_init(&ctx); + if (HMAC_Init_ex(&ctx, key->keyvalue.data, key->keyvalue.length, + EVP_sha1(), NULL) == 0) { + HMAC_CTX_cleanup(&ctx); + return krb5_enomem(context); + } + if (sizeof(digest) != HMAC_size(&ctx)) + krb5_abortx(context, "runtime error, hmac buffer wrong size in kx509"); + HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); + if (req->pk_key.length) + HMAC_Update(&ctx, req->pk_key.data, req->pk_key.length); + else + HMAC_Update(&ctx, req->authenticator.data, req->authenticator.length); + HMAC_Final(&ctx, digest, 0); + HMAC_CTX_cleanup(&ctx); + + if (ct_memcmp(req->pk_hash.data, digest, sizeof(digest)) != 0) { + krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED, + "kx509 request MAC mismatch"); + return KRB5KDC_ERR_PREAUTH_FAILED; + } + return 0; +} + +/* + * Set the HMAC in the response. + */ +static krb5_error_code +calculate_reply_hash(krb5_context context, + krb5_keyblock *key, + Kx509Response *rep) +{ + krb5_error_code ret = 0; + HMAC_CTX ctx; + + HMAC_CTX_init(&ctx); + + if (HMAC_Init_ex(&ctx, key->keyvalue.data, key->keyvalue.length, + EVP_sha1(), NULL) == 0) + ret = krb5_enomem(context); + + if (ret == 0) + ret = krb5_data_alloc(rep->hash, HMAC_size(&ctx)); + if (ret) { + HMAC_CTX_cleanup(&ctx); + return krb5_enomem(context); + } + + HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); + { + int32_t t = rep->error_code; + unsigned char encint[sizeof(t) + 1]; + size_t k; + + /* + * RFC6717 says this about how the error-code is included in the HMAC: + * + * o DER representation of the error-code exclusive of the tag and + * length, if it is present. + * + * So we use der_put_integer(), which encodes from the right. + * + * RFC6717 does not constrain the error-code's range. We assume it to + * be a 32-bit, signed integer, for which we'll need no more than 5 + * bytes. + */ + ret = der_put_integer(&encint[sizeof(encint) - 1], + sizeof(encint), &t, &k); + if (ret == 0) + HMAC_Update(&ctx, &encint[sizeof(encint)] - k, k); + } + if (rep->certificate) + HMAC_Update(&ctx, rep->certificate->data, rep->certificate->length); + if (rep->e_text) + HMAC_Update(&ctx, (unsigned char *)*rep->e_text, strlen(*rep->e_text)); + + HMAC_Final(&ctx, rep->hash->data, 0); + HMAC_CTX_cleanup(&ctx); + + return 0; +} + +static void +frees(char **s) +{ + free(*s); + *s = NULL; +} + +/* Check that a krbtgt's second component is a local realm */ +static krb5_error_code +is_local_realm(krb5_context context, + kx509_req_context reqctx, + const char *realm) +{ + krb5_error_code ret; + krb5_principal tgs; + HDB *db; + hdb_entry *ent = NULL; + + ret = krb5_make_principal(context, &tgs, realm, KRB5_TGS_NAME, realm, + NULL); + if (ret) + return ret; + if (ret == 0) + ret = _kdc_db_fetch(context, reqctx->config, tgs, HDB_F_GET_KRBTGT, + NULL, &db, &ent); + if (ent) + _kdc_free_ent(context, db, ent); + krb5_free_principal(context, tgs); + if (ret == HDB_ERR_NOENTRY || ret == HDB_ERR_NOT_FOUND_HERE) + return KRB5KRB_AP_ERR_NOT_US; + return ret; +} + +/* + * Since we're using the HDB as a keytab we have to check that the client used + * an acceptable name for the kx509 service. + * + * We accept two names: kca_service/hostname and krbtgt/REALM. + * + * We allow cross-realm requests. + * + * Maybe x-realm support should be configurable. Requiring INITIAL tickets + * does NOT preclude x-realm support! (Cross-realm TGTs can be INITIAL.) + * + * Support for specific client realms is configurable by configuring issuer + * credentials and TBS templates on a per-realm basis and configuring no + * default. But maybe we should have an explicit configuration parameter + * to enable support for clients from different realms than the service. + */ +static krb5_error_code +kdc_kx509_verify_service_principal(krb5_context context, + kx509_req_context reqctx, + krb5_principal sprincipal) +{ + krb5_error_code ret = 0; + krb5_principal principal = NULL; + char *expected = NULL; + char localhost[MAXHOSTNAMELEN]; + + if (krb5_principal_get_num_comp(context, sprincipal) != 2) + goto err; + + /* Check if sprincipal is a krbtgt/REALM name */ + if (strcmp(krb5_principal_get_comp_string(context, sprincipal, 0), + KRB5_TGS_NAME) == 0) { + const char *r = krb5_principal_get_comp_string(context, sprincipal, 1); + if ((ret = is_local_realm(context, reqctx, r))) + kdc_audit_addreason((kdc_request_t)reqctx, + "Client used wrong krbtgt for kx509"); + goto out; + } + + /* Must be hostbased kca_service name then */ + ret = gethostname(localhost, sizeof(localhost) - 1); + if (ret != 0) { + ret = errno; + kdc_log(context, reqctx->config, 0, "Failed to get local hostname"); + kdc_audit_addreason((kdc_request_t)reqctx, + "Failed to get local hostname"); + return ret; + } + localhost[sizeof(localhost) - 1] = '\0'; + + ret = krb5_make_principal(context, &principal, "", "kca_service", + localhost, NULL); + if (ret) + goto out; + + if (krb5_principal_compare_any_realm(context, sprincipal, principal)) + goto out; /* found a match */ + +err: + ret = krb5_unparse_name(context, sprincipal, &expected); + if (ret) + goto out; + + ret = KRB5KDC_ERR_SERVER_NOMATCH; + kdc_audit_addreason((kdc_request_t)reqctx, "Client used wrong kx509 " + "service principal (expected %s)", expected); + +out: + krb5_xfree(expected); + krb5_free_principal(context, principal); + + return ret; +} + +static krb5_error_code +encode_reply(krb5_context context, + kx509_req_context reqctx, + Kx509Response *r) +{ + krb5_error_code ret; + krb5_data data; + size_t size; + + reqctx->reply->data = NULL; + reqctx->reply->length = 0; + ASN1_MALLOC_ENCODE(Kx509Response, data.data, data.length, r, &size, ret); + if (ret) + return ret; + if (size != data.length) + krb5_abortx(context, "ASN1 internal error"); + + ret = krb5_data_alloc(reqctx->reply, data.length + sizeof(version_2_0)); + if (ret == 0) { + memcpy(reqctx->reply->data, version_2_0, sizeof(version_2_0)); + memcpy(((unsigned char *)reqctx->reply->data) + sizeof(version_2_0), + data.data, data.length); + } + free(data.data); + return ret; +} + +/* Make an error response, and log the error message as well */ +static krb5_error_code +mk_error_response(krb5_context context, + kx509_req_context reqctx, + int level, + int32_t code, + const char *fmt, + ...) +{ + krb5_error_code ret = code; + krb5_error_code ret2; + Kx509Response rep; + const char *msg; + char *freeme0 = NULL; + char *freeme1 = NULL; + va_list ap; + + if (code != 0) { + /* Log errors where _kdc_audit_trail() is not enough */ + if (code == ENOMEM) + level = 0; + if (level < 3) { + va_start(ap, fmt); + kdc_vlog(context, reqctx->config, level, fmt, ap); + va_end(ap); + } + + va_start(ap, fmt); + kdc_audit_vaddreason((kdc_request_t)reqctx, fmt, ap); + va_end(ap); + } + + if (!reqctx->config->enable_kx509) + code = KRB5KDC_ERR_POLICY; + + /* Make sure we only send RFC4120 and friends wire protocol error codes */ + if (code) { + if (code == KX509_ERR_NONE) { + code = 0; + } else if (code > KX509_ERR_NONE && code <= KX509_ERR_SRV_OVERLOADED) { + code -= KX509_ERR_NONE; + } else { + if (code < KRB5KDC_ERR_NONE || code >= KRB5_ERR_RCSID) + code = KRB5KRB_ERR_GENERIC; + code -= KRB5KDC_ERR_NONE; + code += kx509_krb5_error_base; + } + } + + va_start(ap, fmt); + if (vasprintf(&freeme0, fmt, ap) == -1 || freeme0 == NULL) + msg = "Could not format error message (out of memory)"; + else + msg = freeme0; + va_end(ap); + + if (!reqctx->config->enable_kx509 && + asprintf(&freeme1, "kx509 service is disabled (%s)", msg) > -1 && + freeme1 != NULL) { + msg = freeme1; + } + + rep.hash = NULL; + rep.certificate = NULL; + rep.error_code = code; + if (ALLOC(rep.e_text)) + *rep.e_text = (void *)(uintptr_t)msg; + + if (reqctx->key) { + if (ALLOC(rep.hash) != NULL && + calculate_reply_hash(context, reqctx->key, &rep)) { + free(rep.hash); + rep.hash = NULL; + } + } + + if ((ret2 = encode_reply(context, reqctx, &rep))) + ret = ret2; + if (rep.hash) + krb5_data_free(rep.hash); + free(rep.e_text); + free(rep.hash); + free(freeme0); + free(freeme1); + return ret; +} + +/* Wrap a bare public (RSA) key with a CSR (not signed it, since we can't) */ +static krb5_error_code +make_csr(krb5_context context, kx509_req_context reqctx, krb5_data *key) +{ + krb5_error_code ret; + SubjectPublicKeyInfo spki; + heim_any any; + + ret = hx509_request_init(context->hx509ctx, &reqctx->csr); + if (ret) + return ret; + + memset(&spki, 0, sizeof(spki)); + spki.subjectPublicKey.data = key->data; + spki.subjectPublicKey.length = key->length * 8; + + ret = der_copy_oid(&asn1_oid_id_pkcs1_rsaEncryption, + &spki.algorithm.algorithm); + + any.data = "\x05\x00"; + any.length = 2; + spki.algorithm.parameters = &any; + + if (ret == 0) + ret = hx509_request_set_SubjectPublicKeyInfo(context->hx509ctx, + reqctx->csr, &spki); + der_free_oid(&spki.algorithm.algorithm); + if (ret) + hx509_request_free(&reqctx->csr); + + /* + * TODO: Move a lot of the templating stuff here so we can let clients + * leave out extensions they don't want. + */ + return ret; +} + +/* Update a CSR with desired Certificate Extensions */ +static krb5_error_code +update_csr(krb5_context context, kx509_req_context reqctx, Extensions *exts) +{ + krb5_error_code ret = 0; + size_t i, k; + + if (exts == NULL) + return 0; + + for (i = 0; ret == 0 && i < exts->len; i++) { + Extension *e = &exts->val[i]; + + if (der_heim_oid_cmp(&e->extnID, &asn1_oid_id_x509_ce_keyUsage) == 0) { + KeyUsage ku; + + ret = decode_KeyUsage(e->extnValue.data, e->extnValue.length, &ku, + NULL); + if (ret) + return ret; + ret = hx509_request_set_ku(context->hx509ctx, reqctx->csr, ku); + } else if (der_heim_oid_cmp(&e->extnID, + &asn1_oid_id_x509_ce_extKeyUsage) == 0) { + ExtKeyUsage eku; + + ret = decode_ExtKeyUsage(e->extnValue.data, e->extnValue.length, + &eku, NULL); + for (k = 0; ret == 0 && k < eku.len; k++) { + ret = hx509_request_add_eku(context->hx509ctx, reqctx->csr, + &eku.val[k]); + } + free_ExtKeyUsage(&eku); + } else if (der_heim_oid_cmp(&e->extnID, + &asn1_oid_id_x509_ce_subjectAltName) == 0) { + GeneralNames san; + + ret = decode_GeneralNames(e->extnValue.data, e->extnValue.length, + &san, NULL); + for (k = 0; ret == 0 && k < san.len; k++) { + ret = hx509_request_add_GeneralName(context->hx509ctx, + reqctx->csr, &san.val[k]); + } + free_GeneralNames(&san); + } + } + if (ret) { + const char *emsg = krb5_get_error_message(context, ret); + kdc_log(context, reqctx->config, 1, + "Error handling requested extensions: %s", emsg); + kdc_audit_addreason((kdc_request_t)reqctx, + "Error handling requested extensions: %s", + emsg); + krb5_free_error_message(context, emsg); + } + return ret; +} + + +/* + * Parse the `pk_key' from the request as a CSR or raw public key, and if the + * latter, wrap it in a non-signed CSR. + */ +static krb5_error_code +get_csr(krb5_context context, kx509_req_context reqctx) +{ + krb5_error_code ret; + RSAPublicKey rsapkey; + heim_octet_string pk_key = reqctx->req.pk_key; + size_t size; + + ret = decode_Kx509CSRPlus(pk_key.data, pk_key.length, &reqctx->csr_plus, + &size); + if (ret == 0) { + reqctx->have_csr = 1; + reqctx->send_chain = 1; + + /* Parse CSR */ + ret = hx509_request_parse_der(context->hx509ctx, &reqctx->csr_plus.csr, + &reqctx->csr); + /* + * Handle any additional Certificate Extensions requested out of band + * of the CSR. + */ + if (ret == 0) + return update_csr(context, reqctx, reqctx->csr_plus.exts); + kdc_audit_addreason((kdc_request_t)reqctx, "Invalid CSR"); + return ret; + } + reqctx->send_chain = 0; + reqctx->have_csr = 0; + + /* Check if proof of possession is required by configuration */ + if (!get_bool_param(context, FALSE, reqctx->realm, "require_csr")) { + kdc_audit_addreason((kdc_request_t)reqctx, + "CSRs required but client did not send one"); + krb5_set_error_message(context, KX509_STATUS_CLIENT_USE_CSR, + "CSRs required but kx509 client did not send " + "one"); + return KX509_STATUS_CLIENT_USE_CSR; + } + + /* Attempt to decode pk_key as RSAPublicKey */ + ret = decode_RSAPublicKey(reqctx->req.pk_key.data, + reqctx->req.pk_key.length, + &rsapkey, &size); + free_RSAPublicKey(&rsapkey); + if (ret == 0 && size == reqctx->req.pk_key.length) + return make_csr(context, reqctx, &pk_key); /* Make pretend CSR */ + + /* Not an RSAPublicKey or garbage follows it */ + if (ret == 0) { + ret = KRB5KDC_ERR_NULL_KEY; + kdc_audit_addreason((kdc_request_t)reqctx, + "Request has garbage after key"); + krb5_set_error_message(context, ret, "Request has garbage after key"); + return ret; + } + + kdc_audit_addreason((kdc_request_t)reqctx, + "Could not decode CSR or RSA subject public key"); + krb5_set_error_message(context, ret, + "Could not decode CSR or RSA subject public key"); + return ret; +} + +/* + * Host-based principal _clients_ might ask for a cert for their host -- but + * which services are permitted to do that? This function answers that + * question. + */ +static int +check_authz_svc_ok(krb5_context context, const char *svc) +{ + const char *def[] = { "host", "HTTP", 0 }; + const char * const *svcs; + char **strs; + + strs = krb5_config_get_strings(context, NULL, "kdc", + "kx509_permitted_hostbased_services", NULL); + for (svcs = strs ? (const char * const *)strs : def; svcs[0]; svcs++) { + if (strcmp(svcs[0], svc) == 0) { + krb5_config_free_strings(strs); + return 1; + } + } + krb5_config_free_strings(strs); + return 0; +} + +static krb5_error_code +check_authz(krb5_context context, + kx509_req_context reqctx, + krb5_principal cprincipal) +{ + krb5_error_code ret; + const char *comp0 = krb5_principal_get_comp_string(context, cprincipal, 0); + const char *comp1 = krb5_principal_get_comp_string(context, cprincipal, 1); + unsigned int ncomp = krb5_principal_get_num_comp(context, cprincipal); + hx509_san_type san_type; + KeyUsage ku, ku_allowed; + size_t i; + const heim_oid *eku_whitelist[] = { + &asn1_oid_id_pkix_kp_serverAuth, + &asn1_oid_id_pkix_kp_clientAuth, + &asn1_oid_id_pkekuoid, + &asn1_oid_id_pkinit_ms_eku + }; + char *cprinc = NULL; + char *s = NULL; + + /* + * In the no-CSR case we'll derive cert contents from client name and its + * HDB entry -- authorization is implied. + */ + if (!reqctx->have_csr) + return 0; + ret = kdc_authorize_csr(context, reqctx->config->app, reqctx->csr, + cprincipal); + if (ret == 0) { + kdc_audit_setkv_bool((kdc_request_t)reqctx, "authorized", TRUE); + + ret = hx509_request_get_san(reqctx->csr, 0, &san_type, &s); + if (ret == 0) { + const char *san_type_s; + + /* This should be an hx509 function... */ + switch (san_type) { + case HX509_SAN_TYPE_EMAIL: san_type_s = "rfc822Name"; break; + case HX509_SAN_TYPE_DNSNAME: san_type_s = "dNSName"; break; + case HX509_SAN_TYPE_DN: san_type_s = "DN"; break; + case HX509_SAN_TYPE_REGISTERED_ID: san_type_s = "registeredID"; break; + case HX509_SAN_TYPE_XMPP: san_type_s = "xMPPName"; break; + case HX509_SAN_TYPE_PKINIT: san_type_s = "krb5PrincipalName"; break; + case HX509_SAN_TYPE_MS_UPN: san_type_s = "ms-UPN"; break; + default: san_type_s = "unknown"; break; + } + kdc_audit_addkv((kdc_request_t)reqctx, 0, "san0_type", "%s", + san_type_s); + kdc_audit_addkv((kdc_request_t)reqctx, 0, "san0", "%s", s); + } + frees(&s); + ret = hx509_request_get_eku(reqctx->csr, 0, &s); + if (ret == 0) + kdc_audit_addkv((kdc_request_t)reqctx, 0, "eku0", "%s", s); + free(s); + return 0; + } + if (ret != KRB5_PLUGIN_NO_HANDLE) { + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested extensions rejected by plugin"); + return ret; + } + + /* Default authz */ + if ((ret = krb5_unparse_name(context, cprincipal, &cprinc))) + return ret; + + for (i = 0; ret == 0; i++) { + + frees(&s); + ret = hx509_request_get_san(reqctx->csr, i, &san_type, &s); + if (ret) + break; + switch (san_type) { + case HX509_SAN_TYPE_DNSNAME: + if (ncomp != 2 || strcasecmp(comp1, s) != 0 || + strchr(s, '.') == NULL || + !check_authz_svc_ok(context, comp0)) { + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested extensions rejected by " + "default policy (dNSName SAN " + "does not match client)"); + goto eacces; + } + break; + case HX509_SAN_TYPE_PKINIT: + if (strcmp(cprinc, s) != 0) { + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested extensions rejected by " + "default policy (PKINIT SAN " + "does not match client)"); + goto eacces; + } + break; + default: + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested extensions rejected by " + "default policy (non-default SAN " + "requested)"); + goto eacces; + } + } + frees(&s); + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret) + goto out; + + for (i = 0; ret == 0; i++) { + heim_oid oid; + size_t k; + + frees(&s); + ret = hx509_request_get_eku(reqctx->csr, i, &s); + if (ret) + break; + + if ((ret = der_parse_heim_oid(s, ".", &oid))) { + goto out; + } + for (k = 0; k < sizeof(eku_whitelist)/sizeof(eku_whitelist[0]); k++) { + if (der_heim_oid_cmp(eku_whitelist[k], &oid) == 0) + break; + } + der_free_oid(&oid); + if (k == sizeof(eku_whitelist)/sizeof(eku_whitelist[0])) { + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested EKU rejected by default policy"); + goto eacces; + } + } + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret) + goto out; + + memset(&ku_allowed, 0, sizeof(ku_allowed)); + ku_allowed.digitalSignature = 1; + ku_allowed.nonRepudiation = 1; + ret = hx509_request_get_ku(context->hx509ctx, reqctx->csr, &ku); + if (ret) + goto out; + if (KeyUsage2int(ku) != (KeyUsage2int(ku) & KeyUsage2int(ku_allowed))) + goto eacces; + + kdc_audit_setkv_bool((kdc_request_t)reqctx, "authorized", TRUE); + free(cprinc); + return 0; + +eacces: + ret = EACCES; + goto out2; + +out: + /* XXX Display error code */ + kdc_audit_addreason((kdc_request_t)reqctx, + "Error handling requested extensions"); +out2: + free(cprinc); + free(s); + return ret; +} + +static int +chain_add1_func(hx509_context context, void *d, hx509_cert c) +{ + heim_octet_string os; + Certificates *cs = d; + Certificate c2; + int ret; + + ret = hx509_cert_binary(context, c, &os); + if (ret) + return ret; + ret = decode_Certificate(os.data, os.length, &c2, NULL); + der_free_octet_string(&os); + if (ret) + return ret; + ret = add_Certificates(cs, &c2); + free_Certificate(&c2); + return ret; +} + +static krb5_error_code +encode_cert_and_chain(hx509_context hx509ctx, + hx509_certs certs, + krb5_data *out) +{ + krb5_error_code ret; + Certificates cs; + size_t len; + + cs.len = 0; + cs.val = 0; + + ret = hx509_certs_iter_f(hx509ctx, certs, chain_add1_func, &cs); + if (ret == 0) + ASN1_MALLOC_ENCODE(Certificates, out->data, out->length, + &cs, &len, ret); + free_Certificates(&cs); + return ret; +} + +/* + * Process a request, produce a reply. + */ + +krb5_error_code +_kdc_do_kx509(kx509_req_context r) +{ + krb5_error_code ret = 0; + krb5_ticket *ticket = NULL; + krb5_flags ap_req_options; + krb5_principal cprincipal = NULL; + krb5_principal sprincipal = NULL; + krb5_keytab id = NULL; + Kx509Response rep; + hx509_certs certs = NULL; + int is_probe = 0; + + r->csr_plus.csr.data = NULL; + r->csr_plus.exts = NULL; + r->sname = NULL; + r->cname = NULL; + r->realm = NULL; + r->key = NULL; + r->csr = NULL; + r->ac = NULL; + + /* + * In order to support authenticated error messages we defer checking + * whether the kx509 service is enabled until after accepting the AP-REQ. + */ + + krb5_data_zero(r->reply); + memset(&rep, 0, sizeof(rep)); + + if (r->req.authenticator.length == 0) { + /* + * Unauthenticated kx509 service availability probe. + * + * mk_error_response() will check whether the service is enabled and + * possibly change the error code and message. + */ + is_probe = 1; + kdc_audit_addkv((kdc_request_t)r, 0, "probe", "unauthenticated"); + ret = mk_error_response(r->context, r, 4, 0, + "kx509 service is available"); + goto out; + } + + /* Authenticate the request (consume the AP-REQ) */ + ret = krb5_kt_resolve(r->context, "HDBGET:", &id); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + ret = mk_error_response(r->context, r, 1, + KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, + "Can't open HDB/keytab for kx509: %s", + msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + ret = krb5_rd_req(r->context, + &r->ac, + &r->req.authenticator, + NULL, + id, + &ap_req_options, + &ticket); + if (ret == 0) + ret = krb5_auth_con_getkey(r->context, r->ac, &r->key); + if (ret == 0 && r->key == NULL) + ret = KRB5KDC_ERR_NULL_KEY; + /* + * Provided we got the session key, errors past this point will be + * authenticated. + */ + if (ret == 0) + ret = krb5_ticket_get_client(r->context, ticket, &cprincipal); + + /* Optional: check if Ticket is INITIAL */ + if (ret == 0 && + !ticket->ticket.flags.initial && + !get_bool_param(r->context, TRUE, + krb5_principal_get_realm(r->context, cprincipal), + "require_initial_kca_tickets")) { + ret = mk_error_response(r->context, r, 4, KRB5KDC_ERR_POLICY, + "Client used non-INITIAL tickets, but kx509 " + "service is configured to require INITIAL " + "tickets"); + goto out; + } + + if (ret == 0) + ret = krb5_unparse_name(r->context, cprincipal, &r->cname); + + /* Check that the service name is a valid kx509 service name */ + if (ret == 0) + ret = krb5_ticket_get_server(r->context, ticket, &sprincipal); + if (ret == 0) + r->realm = krb5_principal_get_realm(r->context, sprincipal); + if (ret == 0) + ret = krb5_unparse_name(r->context, sprincipal, &r->sname); + if (ret == 0) + ret = kdc_kx509_verify_service_principal(r->context, r, sprincipal); + if (ret) { + ret = mk_error_response(r->context, r, 4, ret, + "kx509 client used incorrect service name"); + goto out; + } + + /* Authenticate the rest of the request */ + ret = verify_req_hash(r->context, &r->req, r->key); + if (ret) { + ret = mk_error_response(r->context, r, 4, ret, + "Incorrect request HMAC on kx509 request"); + goto out; + } + + if (r->req.pk_key.length == 0) { + /* + * The request is an authenticated kx509 service availability probe. + * + * mk_error_response() will check whether the service is enabled and + * possibly change the error code and message. + */ + is_probe = 1; + kdc_audit_addkv((kdc_request_t)r, 0, "probe", "authenticated"); + ret = mk_error_response(r->context, r, 4, 0, + "kx509 authenticated probe request"); + goto out; + } + + /* Extract and parse CSR or a DER-encoded RSA public key */ + ret = get_csr(r->context, r); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + ret = mk_error_response(r->context, r, 3, ret, + "Failed to parse CSR: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + /* Authorize the request */ + ret = check_authz(r->context, r, cprincipal); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + ret = mk_error_response(r->context, r, 3, ret, + "Rejected by policy: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + /* Issue the certificate */ + ALLOC(rep.hash); + ALLOC(rep.certificate); + if (rep.certificate == NULL || rep.hash == NULL) { + ret = mk_error_response(r->context, r, 0, ENOMEM, + "Could allocate memory for response"); + goto out; + } + + krb5_data_zero(rep.hash); + krb5_data_zero(rep.certificate); + krb5_ticket_get_times(r->context, ticket, &r->ticket_times); + ret = kdc_issue_certificate(r->context, r->config->app, r->logf, r->csr, + cprincipal, &r->ticket_times, 0 /*req_life*/, + r->send_chain, &certs); + if (ret) { + int level = 1; + const char *msg = krb5_get_error_message(r->context, ret); + + if (ret == KRB5KDC_ERR_POLICY) + level = 4; /* _kdc_audit_trail() logs at level 3 */ + ret = mk_error_response(r->context, r, level, ret, + "Certificate isuance failed: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + ret = encode_cert_and_chain(r->context->hx509ctx, certs, rep.certificate); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + ret = mk_error_response(r->context, r, 1, ret, + "Could not encode certificate and chain: %s", + msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + /* Authenticate the response */ + ret = calculate_reply_hash(r->context, r->key, &rep); + if (ret) { + ret = mk_error_response(r->context, r, 1, ret, + "Failed to compute response HMAC"); + goto out; + } + + /* Encode and output reply */ + ret = encode_reply(r->context, r, &rep); + if (ret) + /* Can't send an error message either in this case, surely */ + kdc_audit_addreason((kdc_request_t)r, "Could not encode response"); + +out: + hx509_certs_free(&certs); + if (ret == 0 && !is_probe) + kdc_audit_setkv_bool((kdc_request_t)r, "cert_issued", TRUE); + else + kdc_audit_setkv_bool((kdc_request_t)r, "cert_issued", FALSE); + if (r->ac) + krb5_auth_con_free(r->context, r->ac); + if (ticket) + krb5_free_ticket(r->context, ticket); + if (id) + krb5_kt_close(r->context, id); + if (sprincipal) + krb5_free_principal(r->context, sprincipal); + if (cprincipal) + krb5_free_principal(r->context, cprincipal); + if (r->key) + krb5_free_keyblock (r->context, r->key); + hx509_request_free(&r->csr); + free_Kx509CSRPlus(&r->csr_plus); + free_Kx509Response(&rep); + free_Kx509Request(&r->req); + + return ret; +} + +#endif /* KX509 */ diff --git a/third_party/heimdal/kdc/libkdc-exports.def b/third_party/heimdal/kdc/libkdc-exports.def new file mode 100644 index 0000000..1d42b8c --- /dev/null +++ b/third_party/heimdal/kdc/libkdc-exports.def @@ -0,0 +1,107 @@ +EXPORTS + kdc_authorize_csr + kdc_get_instance + kdc_issue_certificate + kdc_log + kdc_log_msg + kdc_log_msg_va + kdc_openlog + kdc_check_flags + kdc_validate_token + krb5_kdc_plugin_init + krb5_kdc_get_config + krb5_kdc_get_time + krb5_kdc_pkinit_config + krb5_kdc_set_dbinfo + krb5_kdc_process_krb5_request + krb5_kdc_process_request + krb5_kdc_save_request + krb5_kdc_update_time + krb5_kdc_pk_initialize + kdc_request_set_attribute + kdc_request_get_attribute + kdc_request_copy_attribute + kdc_request_delete_attribute + kdc_request_add_encrypted_padata + kdc_request_add_pac_buffer + kdc_request_add_reply_padata + kdc_request_get_addr + kdc_request_get_armor_client + kdc_request_get_armor_clientdb + kdc_request_get_armor_pac + kdc_request_get_armor_server + kdc_request_get_canon_client_princ + kdc_request_get_client + kdc_request_get_clientdb + kdc_request_get_client_princ + kdc_request_get_context + kdc_request_get_config + kdc_request_get_cname + kdc_request_get_error_code + kdc_request_get_explicit_armor_pac + kdc_request_get_explicit_armor_clientdb + kdc_request_get_explicit_armor_client + kdc_request_get_explicit_armor_present + kdc_request_get_explicit_armor_server + kdc_request_get_from + kdc_request_get_krbtgt + kdc_request_get_krbtgtdb + kdc_request_get_krbtgt_princ + kdc_request_get_pac + kdc_request_get_pac_attributes + kdc_request_get_rep + kdc_request_get_reply_key + kdc_request_get_req + kdc_request_get_request + kdc_request_get_server + kdc_request_get_serverdb + kdc_request_get_server_princ + kdc_request_get_sname + kdc_request_get_ticket + kdc_request_get_tv_end + kdc_request_get_tv_start + kdc_request_set_canon_client_princ + kdc_request_set_client_princ + kdc_request_set_cname + kdc_request_set_e_data + kdc_request_set_error_code + kdc_request_set_krbtgt_princ + kdc_request_set_pac + kdc_request_set_pac_attributes + kdc_request_set_rep + kdc_request_set_reply_key + kdc_request_set_server_princ + kdc_request_set_sname + kdc_audit_addkv + kdc_audit_addkv_number + kdc_audit_addkv_object + kdc_audit_addkv_timediff + kdc_audit_addaddrs + kdc_audit_addreason + kdc_audit_getkv + kdc_audit_setkv_bool + kdc_audit_setkv_number + kdc_audit_setkv_object + kdc_audit_vaddkv + kdc_audit_vaddreason + _kdc_audit_trail + + kdc_object_alloc + kdc_object_retain + kdc_object_release + kdc_bool_create + kdc_bool_get_value + kdc_array_iterate + kdc_array_get_length + kdc_array_get_value + kdc_array_copy_value + kdc_string_create + kdc_string_get_utf8 + kdc_data_create + kdc_data_get_data + kdc_number_create + kdc_number_get_value + + ; needed for digest-service + _kdc_db_fetch + _kdc_free_ent diff --git a/third_party/heimdal/kdc/libkdc-version.rc b/third_party/heimdal/kdc/libkdc-version.rc new file mode 100644 index 0000000..fee5004 --- /dev/null +++ b/third_party/heimdal/kdc/libkdc-version.rc @@ -0,0 +1,36 @@ +/*********************************************************************** + * Copyright (c) 2010, Secure Endpoints Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - 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. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 + * COPYRIGHT HOLDER 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 RC_FILE_TYPE VFT_DLL +#define RC_FILE_DESC_0409 "Heimdal KDC Library" +#define RC_FILE_ORIG_0409 "libkdc.dll" + +#include "../windows/version.rc" diff --git a/third_party/heimdal/kdc/log.c b/third_party/heimdal/kdc/log.c new file mode 100644 index 0000000..bfb0f54 --- /dev/null +++ b/third_party/heimdal/kdc/log.c @@ -0,0 +1,110 @@ +/* + * Copyright (c) 1997, 1998, 2002 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2009 Apple Inc. 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. + */ + +#include "kdc_locl.h" + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_openlog(krb5_context context, + const char *service, + krb5_kdc_configuration *config) +{ + char **s = NULL, **p; + krb5_initlog(context, "kdc", &config->logf); + s = krb5_config_get_strings(context, NULL, service, "logging", NULL); + if(s == NULL) + s = krb5_config_get_strings(context, NULL, "logging", service, NULL); + if(s){ + for(p = s; *p; p++) + krb5_addlog_dest(context, config->logf, *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, config->logf, ss); + free(ss); + } + krb5_set_warn_dest(context, config->logf); +} + +#undef __attribute__ +#define __attribute__(X) + +KDC_LIB_FUNCTION char * KDC_LIB_CALL +kdc_log_msg_va(krb5_context context, + krb5_kdc_configuration *config, + int level, const char *fmt, va_list ap) + __attribute__ ((__format__ (__printf__, 4, 0))) +{ + char *msg; + krb5_vlog_msg(context, config->logf, &msg, level, fmt, ap); + return msg; +} + +KDC_LIB_FUNCTION char * KDC_LIB_CALL +kdc_log_msg(krb5_context context, + krb5_kdc_configuration *config, + int level, const char *fmt, ...) + __attribute__ ((__format__ (__printf__, 4, 5))) +{ + va_list ap; + char *s; + va_start(ap, fmt); + s = kdc_log_msg_va(context, config, level, fmt, ap); + va_end(ap); + return s; +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_vlog(krb5_context context, + krb5_kdc_configuration *config, + int level, const char *fmt, va_list ap) + __attribute__ ((__format__ (__printf__, 4, 0))) +{ + free(kdc_log_msg_va(context, config, level, fmt, ap)); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_log(krb5_context context, + krb5_kdc_configuration *config, + int level, const char *fmt, ...) + __attribute__ ((__format__ (__printf__, 4, 5))) +{ + va_list ap; + va_start(ap, fmt); + free(kdc_log_msg_va(context, config, level, fmt, ap)); + va_end(ap); +} diff --git a/third_party/heimdal/kdc/main.c b/third_party/heimdal/kdc/main.c new file mode 100644 index 0000000..a4a7ade --- /dev/null +++ b/third_party/heimdal/kdc/main.c @@ -0,0 +1,181 @@ +/* + * Copyright (c) 1997-2005 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2009 Apple Inc. 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. + */ + +#include "kdc_locl.h" +#ifdef HAVE_UTIL_H +#include <util.h> +#endif + +#ifdef HAVE_CAPNG +#include <cap-ng.h> +#endif + +sig_atomic_t exit_flag = 0; + +int detach_from_console = -1; +int daemon_child = -1; +int do_bonjour = -1; + +static RETSIGTYPE +sigchld(int sig) +{ +} + +static RETSIGTYPE +sigterm(int sig) +{ + exit_flag = sig; +} + +/* + * Allow dropping root bit, since heimdal reopens the database all the + * time the database needs to be owned by the user you are switched + * too. A better solution is to split the kdc in to more processes and + * run the network facing part with very low privilege. + */ + +static void +switch_environment(void) +{ +#ifdef HAVE_GETEUID + if ((runas_string || chroot_string) && geteuid() != 0) + errx(1, "no running as root, can't switch user/chroot"); + + if (chroot_string) { + if (chroot(chroot_string)) + err(1, "chroot(%s) failed", chroot_string); + if (chdir("/")) + err(1, "chdir(/) after chroot failed"); + } + + if (runas_string) { + struct passwd *pw; + + pw = getpwnam(runas_string); + if (pw == NULL) + errx(1, "unknown user %s", runas_string); + + if (initgroups(pw->pw_name, pw->pw_gid) < 0) + err(1, "initgroups failed"); + +#ifndef HAVE_CAPNG + if (setgid(pw->pw_gid) < 0) + err(1, "setgid(%s) failed", runas_string); + + if (setuid(pw->pw_uid) < 0) + err(1, "setuid(%s)", runas_string); +#else + capng_clear (CAPNG_EFFECTIVE | CAPNG_PERMITTED); + if (capng_updatev (CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, + CAP_NET_BIND_SERVICE, CAP_SETPCAP, -1) < 0) + err(1, "capng_updateev"); + + if (capng_change_id(pw->pw_uid, pw->pw_gid, + CAPNG_CLEAR_BOUNDING) < 0) + err(1, "capng_change_id(%s)", runas_string); +#endif + } +#endif +} + +int +main(int argc, char **argv) +{ + krb5_error_code ret; + krb5_context context; + krb5_kdc_configuration *config; + int optidx = 0; + + setprogname(argv[0]); + + ret = krb5_init_context(&context); + if (ret == KRB5_CONFIG_BADFORMAT) + errx (1, "krb5_init_context failed to parse configuration file"); + else if (ret) + errx (1, "krb5_init_context failed: %d", ret); + + ret = krb5_kt_register(context, &hdb_get_kt_ops); + if (ret) + errx (1, "krb5_kt_register(HDB) failed: %d", ret); + + config = configure(context, argc, argv, &optidx); + +#ifdef HAVE_SIGACTION + { + struct sigaction sa; + + sa.sa_flags = 0; + sa.sa_handler = sigterm; + sigemptyset(&sa.sa_mask); + + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); +#ifdef SIGXCPU + sigaction(SIGXCPU, &sa, NULL); +#endif + +#ifdef SIGCHLD + sa.sa_handler = sigchld; + sigaction(SIGCHLD, &sa, NULL); +#endif + + sa.sa_handler = SIG_IGN; +#ifdef SIGPIPE + sigaction(SIGPIPE, &sa, NULL); +#endif + } +#else + signal(SIGINT, sigterm); + signal(SIGTERM, sigterm); +#ifdef SIGCHLD + signal(SIGCHLD, sigchld); +#endif +#ifdef SIGXCPU + signal(SIGXCPU, sigterm); +#endif +#ifdef SIGPIPE + signal(SIGPIPE, SIG_IGN); +#endif +#endif + rk_pidfile(NULL); + + switch_environment(); + + start_kdc(context, config, argv[0]); + _krb5_unload_plugins(context, "kdc"); + krb5_free_context(context); + free(config); + return 0; +} diff --git a/third_party/heimdal/kdc/misc.c b/third_party/heimdal/kdc/misc.c new file mode 100644 index 0000000..00bbe0c --- /dev/null +++ b/third_party/heimdal/kdc/misc.c @@ -0,0 +1,363 @@ +/* + * Copyright (c) 1997 - 2001 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. + */ + +#include "kdc_locl.h" + +static int +name_type_ok(krb5_context context, + krb5_kdc_configuration *config, + krb5_const_principal principal) +{ + int nt = krb5_principal_get_type(context, principal); + + if (!krb5_principal_is_krbtgt(context, principal)) + return 1; + if (nt == KRB5_NT_SRV_INST || nt == KRB5_NT_UNKNOWN) + return 1; + if (config->strict_nametypes == 0) + return 1; + return 0; +} + +struct timeval _kdc_now; + +static krb5_error_code +synthesize_hdb_close(krb5_context context, struct HDB *db) +{ + (void) context; + (void) db; + return 0; +} + +/* + * Synthesize an HDB entry suitable for PKINIT and GSS preauth. + */ +static krb5_error_code +synthesize_client(krb5_context context, + krb5_kdc_configuration *config, + krb5_const_principal princ, + HDB **db, + hdb_entry **h) +{ + static HDB null_db; + krb5_error_code ret; + hdb_entry *e; + + /* Hope this works! */ + null_db.hdb_destroy = synthesize_hdb_close; + null_db.hdb_close = synthesize_hdb_close; + if (db) + *db = &null_db; + + ret = (e = calloc(1, sizeof(*e))) ? 0 : krb5_enomem(context); + if (ret == 0) { + e->flags.client = 1; + e->flags.immutable = 1; + e->flags.virtual = 1; + e->flags.synthetic = 1; + e->flags.do_not_store = 1; + e->kvno = 1; + e->keys.len = 0; + e->keys.val = NULL; + e->created_by.time = time(NULL); + e->modified_by = NULL; + e->valid_start = NULL; + e->valid_end = NULL; + e->pw_end = NULL; + e->etypes = NULL; + e->generation = NULL; + e->extensions = NULL; + } + if (ret == 0) + ret = (e->max_renew = calloc(1, sizeof(*e->max_renew))) ? + 0 : krb5_enomem(context); + if (ret == 0) + ret = (e->max_life = calloc(1, sizeof(*e->max_life))) ? + 0 : krb5_enomem(context); + if (ret == 0) + ret = krb5_copy_principal(context, princ, &e->principal); + if (ret == 0) + ret = krb5_copy_principal(context, princ, &e->created_by.principal); + if (ret == 0) { + /* + * We can't check OCSP in the TGS path, so we can't let tickets for + * synthetic principals live very long. + */ + *(e->max_renew) = config->synthetic_clients_max_renew; + *(e->max_life) = config->synthetic_clients_max_life; + *h = e; + } else if (e) { + hdb_free_entry(context, &null_db, e); + } + return ret; +} + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +_kdc_db_fetch(krb5_context context, + krb5_kdc_configuration *config, + krb5_const_principal principal, + unsigned flags, + krb5uint32 *kvno_ptr, + HDB **db, + hdb_entry **h) +{ + hdb_entry *ent = NULL; + krb5_error_code ret = HDB_ERR_NOENTRY; + int i; + unsigned kvno = 0; + krb5_principal enterprise_principal = NULL; + krb5_const_principal princ; + + *h = NULL; + if (db) + *db = NULL; + + if (!name_type_ok(context, config, principal)) + return HDB_ERR_NOENTRY; + + flags |= HDB_F_DECRYPT; + if (kvno_ptr != NULL && *kvno_ptr != 0) { + kvno = *kvno_ptr; + flags |= HDB_F_KVNO_SPECIFIED; + } else { + flags |= HDB_F_ALL_KVNOS; + } + + ent = calloc(1, sizeof (*ent)); + if (ent == NULL) + return krb5_enomem(context); + + if (principal->name.name_type == KRB5_NT_ENTERPRISE_PRINCIPAL) { + if (principal->name.name_string.len != 1) { + ret = KRB5_PARSE_MALFORMED; + krb5_set_error_message(context, ret, + "malformed request: " + "enterprise name with %d name components", + principal->name.name_string.len); + goto out; + } + ret = krb5_parse_name(context, principal->name.name_string.val[0], + &enterprise_principal); + if (ret) + goto out; + } + + for (i = 0; i < config->num_db; i++) { + HDB *curdb = config->db[i]; + + if (db) + *db = curdb; + + ret = curdb->hdb_open(context, curdb, O_RDONLY, 0); + if (ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 0, "Failed to open database: %s", msg); + krb5_free_error_message(context, msg); + continue; + } + + princ = principal; + if (!(curdb->hdb_capability_flags & HDB_CAP_F_HANDLE_ENTERPRISE_PRINCIPAL) && enterprise_principal) + princ = enterprise_principal; + + ret = hdb_fetch_kvno(context, curdb, princ, flags, 0, 0, kvno, ent); + curdb->hdb_close(context, curdb); + + if (ret == HDB_ERR_NOENTRY) + continue; /* Check the other databases */ + + /* + * This is really important, because errors like + * HDB_ERR_NOT_FOUND_HERE (used to indicate to Samba that + * the RODC on which this code is running does not have + * the key we need, and so a proxy to the KDC is required) + * have specific meaning, and need to be propogated up. + */ + break; + } + + switch (ret) { + case HDB_ERR_WRONG_REALM: + case 0: + /* + * the ent->entry.principal just contains hints for the client + * to retry. This is important for enterprise principal routing + * between trusts. + */ + *h = ent; + ent = NULL; + break; + + case HDB_ERR_NOENTRY: + if (db) + *db = NULL; + if ((flags & HDB_F_GET_CLIENT) && (flags & HDB_F_SYNTHETIC_OK) && + config->synthetic_clients) { + ret = synthesize_client(context, config, principal, db, h); + if (ret) { + krb5_set_error_message(context, ret, "could not synthesize " + "HDB client principal entry"); + ret = HDB_ERR_NOENTRY; + krb5_prepend_error_message(context, ret, "no such entry found in hdb"); + } + } else { + krb5_set_error_message(context, ret, "no such entry found in hdb"); + } + break; + + default: + if (db) + *db = NULL; + break; + } + +out: + krb5_free_principal(context, enterprise_principal); + free(ent); + return ret; +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +_kdc_free_ent(krb5_context context, HDB *db, hdb_entry *ent) +{ + hdb_free_entry (context, db, ent); + free (ent); +} + +/* + * Use the order list of preferred encryption types and sort the + * available keys and return the most preferred key. + */ + +krb5_error_code +_kdc_get_preferred_key(krb5_context context, + krb5_kdc_configuration *config, + hdb_entry *h, + const char *name, + krb5_enctype *enctype, + Key **key) +{ + krb5_error_code ret; + int i; + + if (config->use_strongest_server_key) { + const krb5_enctype *p = krb5_kerberos_enctypes(context); + + for (i = 0; p[i] != ETYPE_NULL; i++) { + if (krb5_enctype_valid(context, p[i]) != 0 && + !_kdc_is_weak_exception(h->principal, p[i])) + continue; + ret = hdb_enctype2key(context, h, NULL, p[i], key); + if (ret != 0) + continue; + if (enctype != NULL) + *enctype = p[i]; + return 0; + } + } else { + *key = NULL; + + for (i = 0; i < h->keys.len; i++) { + if (krb5_enctype_valid(context, h->keys.val[i].key.keytype) != 0 && + !_kdc_is_weak_exception(h->principal, h->keys.val[i].key.keytype)) + continue; + ret = hdb_enctype2key(context, h, NULL, + h->keys.val[i].key.keytype, key); + if (ret != 0) + continue; + if (enctype != NULL) + *enctype = (*key)->key.keytype; + return 0; + } + } + + krb5_set_error_message(context, ret = KRB5KDC_ERR_ETYPE_NOSUPP, + "No valid kerberos key found for %s", name); + return ret; +} + +krb5_error_code +_kdc_verify_checksum(krb5_context context, + krb5_crypto crypto, + krb5_key_usage usage, + const krb5_data *data, + Checksum *cksum) +{ + krb5_error_code ret; + + ret = krb5_verify_checksum(context, crypto, usage, + data->data, data->length, + cksum); + if (ret == KRB5_PROG_SUMTYPE_NOSUPP) + ret = KRB5KDC_ERR_SUMTYPE_NOSUPP; + + return ret; +} + +/* + * Returns TRUE if a PAC should be included in ticket authorization data. + * + * Per [MS-KILE] 3.3.5.3, PACs are always included for TGTs; for service + * tickets, policy is governed by whether the client explicitly requested + * a PAC be omitted when requesting a TGT, or if the no-auth-data-reqd + * flag is set on the service principal entry. + * + * However, when issuing a cross-realm TGT to an AD realm our PAC might not + * interoperate correctly. Therefore we honor the no-auth-data-reqd HDB entry + * flag on cross-realm TGTs. + */ + +krb5_boolean +_kdc_include_pac_p(astgs_request_t r) +{ + return TRUE; +} + +/* + * Notify the HDB backend and KDC plugin of the audited event. + */ + +krb5_error_code +_kdc_audit_request(astgs_request_t r) +{ + krb5_error_code ret; + struct HDB *hdb; + + ret = _kdc_plugin_audit(r); + if (ret == 0 && + (hdb = r->clientdb ? r->clientdb : r->config->db[0]) && + hdb->hdb_audit) + ret = hdb->hdb_audit(r->context, hdb, r->client, (hdb_request_t)r); + + return ret; +} diff --git a/third_party/heimdal/kdc/mit_dump.c b/third_party/heimdal/kdc/mit_dump.c new file mode 100644 index 0000000..af380bb --- /dev/null +++ b/third_party/heimdal/kdc/mit_dump.c @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2000 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. + */ + +#include "hprop.h" + +extern krb5_error_code _hdb_mdb_value2entry(krb5_context context, + krb5_data *data, + krb5_kvno target_kvno, + hdb_entry *entry); + +extern int _hdb_mit_dump2mitdb_entry(krb5_context context, + char *line, + krb5_storage *sp); + + + +/* +can have any number of princ stanzas. +format is as follows (only \n indicates newlines) +princ\t%d\t (%d is KRB5_KDB_V1_BASE_LENGTH, always 38) +%d\t (strlen of principal e.g. shadow/foo@ANDREW.CMU.EDU) +%d\t (number of tl_data) +%d\t (number of key data, e.g. how many keys for this user) +%d\t (extra data length) +%s\t (principal name) +%d\t (attributes) +%d\t (max lifetime, seconds) +%d\t (max renewable life, seconds) +%d\t (expiration, seconds since epoch or 2145830400 for never) +%d\t (password expiration, seconds, 0 for never) +%d\t (last successful auth, seconds since epoch) +%d\t (last failed auth, per above) +%d\t (failed auth count) +foreach tl_data 0 to number of tl_data - 1 as above + %d\t%d\t (data type, data length) + foreach tl_data 0 to length-1 + %02x (tl data contents[element n]) + except if tl_data length is 0 + %d (always -1) + \t +foreach key 0 to number of keys - 1 as above + %d\t%d\t (key data version, kvno) + foreach version 0 to key data version - 1 (a key or a salt) + %d\t%d\t(data type for this key, data length for this key) + foreach key data length 0 to length-1 + %02x (key data contents[element n]) + except if key_data length is 0 + %d (always -1) + \t +foreach extra data length 0 to length - 1 + %02x (extra data part) +unless no extra data + %d (always -1) +;\n + +*/ + +static char * +nexttoken(char **p) +{ + char *q; + do { + q = strsep(p, " \t"); + } while(q && *q == '\0'); + return q; +} + +#include <kadm5/admin.h> + +/* XXX: Principal names with '\n' cannot be dumped or loaded */ +static int +my_fgetln(FILE *f, char **bufp, size_t *szp, size_t *lenp) +{ + size_t len; + size_t sz = *szp; + char *buf = *bufp; + char *n; + + if (!buf) { + buf = malloc(sz ? sz : 8192); + if (!buf) + return ENOMEM; + if (!sz) + sz = 8192; + } + + len = 0; + while (fgets(&buf[len], sz-len, f) != NULL) { + len += strlen(&buf[len]); + if (buf[len-1] == '\n') + break; + if (feof(f)) + break; + if (sz > SIZE_MAX/2 || + (n = realloc(buf, sz += 1 + (sz >> 1))) == NULL) { + free(buf); + *bufp = NULL; + *szp = 0; + *lenp = 0; + return ENOMEM; + } + buf = n; + } + *bufp = buf; + *szp = sz; + *lenp = len; + return 0; /* *len == 0 || no EOL -> EOF */ +} + +int +mit_prop_dump(void *arg, const char *file) +{ + krb5_error_code ret; + size_t line_bufsz = 0; + size_t line_len = 0; + char *line = NULL; + int lineno = 0; + FILE *f; + hdb_entry ent; + struct prop_data *pd = arg; + krb5_storage *sp = NULL; + krb5_data kdb_ent; + + memset(&ent, 0, sizeof (ent)); + f = fopen(file, "r"); + if (f == NULL) + return errno; + + ret = ENOMEM; + sp = krb5_storage_emem(); + if (!sp) + goto out; + while ((ret = my_fgetln(f, &line, &line_bufsz, &line_len)) == 0 && + line_len > 0) { + char *p = line; + char *q; + + lineno++; + if(strncmp(line, "kdb5_util", strlen("kdb5_util")) == 0) { + int major; + q = nexttoken(&p); + if (strcmp(q, "kdb5_util") != 0) + errx(1, "line %d: unknown version", lineno); + q = nexttoken(&p); /* load_dump */ + if (strcmp(q, "load_dump") != 0) + errx(1, "line %d: unknown version", lineno); + q = nexttoken(&p); /* load_dump */ + if (strcmp(q, "version") != 0) + errx(1, "line %d: unknown version", lineno); + q = nexttoken(&p); /* x.0 */ + if (sscanf(q, "%d", &major) != 1) + errx(1, "line %d: unknown version", lineno); + if (major != 4 && major != 5 && major != 6) + errx(1, "unknown dump file format, got %d, expected 4-6", + major); + continue; + } else if(strncmp(p, "policy", strlen("policy")) == 0) { + warnx("line: %d: ignoring policy (not supported)", lineno); + continue; + } else if(strncmp(p, "princ", strlen("princ")) != 0) { + warnx("line %d: not a principal", lineno); + continue; + } + krb5_storage_truncate(sp, 0); + ret = _hdb_mit_dump2mitdb_entry(pd->context, line, sp); + if (ret) { + if (ret > 0) + warn("line: %d: failed to parse; ignoring", lineno); + else + warnx("line: %d: failed to parse; ignoring", lineno); + continue; + } + ret = krb5_storage_to_data(sp, &kdb_ent); + if (ret) break; + ret = _hdb_mdb_value2entry(pd->context, &kdb_ent, 0, &ent); + krb5_data_free(&kdb_ent); + if (ret) { + warnx("line: %d: failed to store; ignoring", lineno); + continue; + } + ret = v5_prop(pd->context, NULL, &ent, arg); + hdb_free_entry(pd->context, NULL, &ent); /* XXX */ + if (ret) break; + } + +out: + fclose(f); + free(line); + if (sp) + krb5_storage_free(sp); + if (ret && ret == ENOMEM) + errx(1, "out of memory"); + if (ret) + errx(1, "line %d: problem parsing dump line", lineno); + return ret; +} + diff --git a/third_party/heimdal/kdc/mssfu.c b/third_party/heimdal/kdc/mssfu.c new file mode 100644 index 0000000..471e193 --- /dev/null +++ b/third_party/heimdal/kdc/mssfu.c @@ -0,0 +1,693 @@ +/* + * Copyright (c) 1997-2008 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. + */ + +#include "kdc_locl.h" + +/* + * [MS-SFU] Kerberos Protocol Extensions: + * Service for User (S4U2Self) and Constrained Delegation Protocol (S4U2Proxy) + * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/ + */ + +/* + * Determine if constrained delegation is allowed from this client to this server + */ + +static krb5_error_code +check_constrained_delegation(krb5_context context, + krb5_kdc_configuration *config, + HDB *clientdb, + hdb_entry *client, + hdb_entry *server, + krb5_const_principal target) +{ + const HDB_Ext_Constrained_delegation_acl *acl; + krb5_error_code ret; + size_t i; + + /* + * constrained delegation (S4U2Proxy) only works within + * the same realm. We use the already canonicalized version + * of the principals here, while "target" is the principal + * provided by the client. + */ + if (!krb5_realm_compare(context, client->principal, server->principal)) { + ret = KRB5KDC_ERR_BADOPTION; + kdc_log(context, config, 4, + "Bad request for constrained delegation"); + return ret; + } + + if (clientdb->hdb_check_constrained_delegation) { + ret = clientdb->hdb_check_constrained_delegation(context, clientdb, client, target); + if (ret == 0) + return 0; + } else { + /* if client delegates to itself, that ok */ + if (krb5_principal_compare(context, client->principal, server->principal) == TRUE) + return 0; + + ret = hdb_entry_get_ConstrainedDelegACL(client, &acl); + if (ret) { + krb5_clear_error_message(context); + return ret; + } + + if (acl) { + for (i = 0; i < acl->len; i++) { + if (krb5_principal_compare(context, target, &acl->val[i]) == TRUE) + return 0; + } + } + ret = KRB5KDC_ERR_BADOPTION; + } + kdc_log(context, config, 4, + "Bad request for constrained delegation"); + return ret; +} + +/* + * Determine if resource-based constrained delegation is allowed from this + * client to this server + */ + +static krb5_error_code +check_rbcd(krb5_context context, + krb5_kdc_configuration *config, + HDB *clientdb, + krb5_const_principal s4u_principal, + const hdb_entry *client_krbtgt, + const hdb_entry *client, + const hdb_entry *device_krbtgt, + const hdb_entry *device, + krb5_const_pac client_pac, + krb5_const_pac device_pac, + const hdb_entry *target) +{ + krb5_error_code ret = KRB5KDC_ERR_BADOPTION; + + if (clientdb->hdb_check_rbcd) { + ret = clientdb->hdb_check_rbcd(context, + clientdb, + client_krbtgt, + client, + device_krbtgt, + device, + s4u_principal, + client_pac, + device_pac, + target); + if (ret == 0) + return 0; + } + + kdc_log(context, config, 4, + "Bad request for resource-based constrained delegation"); + return ret; +} + +/* + * Validate a protocol transition (S4U2Self) request. If successfully + * validated then the client in the request structure will be replaced + * with the impersonated client. + */ + +krb5_error_code +_kdc_validate_protocol_transition(astgs_request_t r, const PA_DATA *for_user) +{ + krb5_error_code ret; + KDC_REQ_BODY *b = &r->req.req_body; + EncTicketPart *ticket = &r->ticket->ticket; + hdb_entry *s4u_client = NULL; + HDB *s4u_clientdb; + int flags = HDB_F_FOR_TGS_REQ; + krb5_principal s4u_client_name = NULL, s4u_canon_client_name = NULL; + krb5_pac s4u_pac = NULL; + char *s4ucname = NULL; + krb5_crypto crypto; + krb5_data datack; + PA_S4U2Self self; + const char *str; + + heim_assert(r->client != NULL, "client must be non-NULL"); + + memset(&self, 0, sizeof(self)); + + if (b->kdc_options.canonicalize) + flags |= HDB_F_CANON; + + ret = decode_PA_S4U2Self(for_user->padata_value.data, + for_user->padata_value.length, + &self, NULL); + if (ret) { + kdc_audit_addreason((kdc_request_t)r, + "Failed to decode PA-S4U2Self"); + kdc_log(r->context, r->config, 4, "Failed to decode PA-S4U2Self"); + goto out; + } + + if (!krb5_checksum_is_keyed(r->context, self.cksum.cksumtype)) { + kdc_audit_addreason((kdc_request_t)r, + "PA-S4U2Self with unkeyed checksum"); + kdc_log(r->context, r->config, 4, "Reject PA-S4U2Self with unkeyed checksum"); + ret = KRB5KRB_AP_ERR_INAPP_CKSUM; + goto out; + } + + ret = _krb5_s4u2self_to_checksumdata(r->context, &self, &datack); + if (ret) + goto out; + + ret = krb5_crypto_init(r->context, &ticket->key, 0, &crypto); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + krb5_data_free(&datack); + kdc_log(r->context, r->config, 4, "krb5_crypto_init failed: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + /* Allow HMAC_MD5 checksum with any key type */ + if (self.cksum.cksumtype == CKSUMTYPE_HMAC_MD5) { + struct krb5_crypto_iov iov; + unsigned char csdata[16]; + Checksum cs; + + cs.checksum.length = sizeof(csdata); + cs.checksum.data = &csdata; + + iov.data.data = datack.data; + iov.data.length = datack.length; + iov.flags = KRB5_CRYPTO_TYPE_DATA; + + ret = _krb5_HMAC_MD5_checksum(r->context, NULL, &crypto->key, + KRB5_KU_OTHER_CKSUM, &iov, 1, + &cs); + if (ret == 0 && + krb5_data_ct_cmp(&cs.checksum, &self.cksum.checksum) != 0) + ret = KRB5KRB_AP_ERR_BAD_INTEGRITY; + } else { + ret = _kdc_verify_checksum(r->context, + crypto, + KRB5_KU_OTHER_CKSUM, + &datack, + &self.cksum); + } + krb5_data_free(&datack); + krb5_crypto_destroy(r->context, crypto); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + kdc_audit_addreason((kdc_request_t)r, + "S4U2Self checksum failed"); + kdc_log(r->context, r->config, 4, + "krb5_verify_checksum failed for S4U2Self: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + ret = _krb5_principalname2krb5_principal(r->context, + &s4u_client_name, + self.name, + self.realm); + if (ret) + goto out; + + ret = krb5_unparse_name(r->context, s4u_client_name, &s4ucname); + if (ret) + goto out; + + /* + * Note no HDB_F_SYNTHETIC_OK -- impersonating non-existent clients + * is probably not desirable! + */ + ret = _kdc_db_fetch(r->context, r->config, s4u_client_name, + HDB_F_GET_CLIENT | flags, NULL, + &s4u_clientdb, &s4u_client); + if (ret) { + const char *msg; + + /* + * If the client belongs to the same realm as our krbtgt, it + * should exist in the local database. + * + */ + if (ret == HDB_ERR_NOENTRY) + ret = KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN; + msg = krb5_get_error_message(r->context, ret); + kdc_audit_addreason((kdc_request_t)r, + "S4U2Self principal to impersonate not found"); + kdc_log(r->context, r->config, 2, + "S4U2Self principal to impersonate %s not found in database: %s", + s4ucname, msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + /* + * Ignore require_pwchange and pw_end attributes (as Windows does), + * since S4U2Self is not password authentication. + */ + s4u_client->flags.require_pwchange = FALSE; + free(s4u_client->pw_end); + s4u_client->pw_end = NULL; + + ret = kdc_check_flags(r, FALSE, s4u_client, r->server); + if (ret) + goto out; /* kdc_check_flags() calls kdc_audit_addreason() */ + + ret = _kdc_pac_generate(r, + s4u_client, + r->server, + NULL, + KRB5_PAC_WAS_GIVEN_IMPLICITLY, + &s4u_pac); + if (ret) { + kdc_log(r->context, r->config, 4, "PAC generation failed for -- %s", s4ucname); + goto out; + } + + /* + * Check that service doing the impersonating is + * requesting a ticket to it-self. + */ + ret = _kdc_check_client_matches_target_service(r->context, + r->config, + r->clientdb, + r->client, + r->server, + r->server_princ); + if (ret) { + kdc_log(r->context, r->config, 4, "S4U2Self: %s is not allowed " + "to impersonate to service " + "(tried for user %s to service %s)", + r->cname, s4ucname, r->sname); + goto out; + } + + ret = krb5_copy_principal(r->context, s4u_client->principal, + &s4u_canon_client_name); + if (ret) + goto out; + + /* + * If the service isn't trusted for authentication to + * delegation or if the impersonate client is disallowed + * forwardable, remove the forwardable flag. + */ + if (r->client->flags.trusted_for_delegation && + s4u_client->flags.forwardable) { + str = " [forwardable]"; + } else { + b->kdc_options.forwardable = 0; + str = ""; + } + kdc_log(r->context, r->config, 4, "s4u2self %s impersonating %s to " + "service %s%s", r->cname, s4ucname, r->sname, str); + + /* + * Replace all client information in the request with the + * impersonated client. (The audit entry containing the original + * client name will have been created before this point.) + */ + _kdc_request_set_cname_nocopy((kdc_request_t)r, &s4ucname); + _kdc_request_set_client_princ_nocopy(r, &s4u_client_name); + + _kdc_free_ent(r->context, r->clientdb, r->client); + r->client = s4u_client; + s4u_client = NULL; + r->clientdb = s4u_clientdb; + s4u_clientdb = NULL; + + _kdc_request_set_canon_client_princ_nocopy(r, &s4u_canon_client_name); + _kdc_request_set_pac_nocopy(r, &s4u_pac); + +out: + if (s4u_client) + _kdc_free_ent(r->context, s4u_clientdb, s4u_client); + krb5_free_principal(r->context, s4u_client_name); + krb5_xfree(s4ucname); + krb5_free_principal(r->context, s4u_canon_client_name); + krb5_pac_free(r->context, s4u_pac); + + free_PA_S4U2Self(&self); + + return ret; +} + +/* + * Validate a constrained delegation (S4U2Proxy) request. If + * successfully validated then the client in the request structure will + * be replaced with the client from the evidence ticket. + */ + +krb5_error_code +_kdc_validate_constrained_delegation(astgs_request_t r) +{ + krb5_error_code ret; + KDC_REQ_BODY *b = &r->req.req_body; + int flags = HDB_F_FOR_TGS_REQ; + krb5_principal s4u_client_name = NULL, s4u_server_name = NULL; + krb5_principal s4u_canon_client_name = NULL; + krb5_pac s4u_pac = NULL; + uint64_t s4u_pac_attributes; + char *s4ucname = NULL, *s4usname = NULL; + EncTicketPart evidence_tkt; + HDB *s4u_clientdb; + hdb_entry *s4u_client = NULL; + HDB *s4u_serverdb = NULL; + hdb_entry *s4u_server = NULL; + krb5_boolean ad_kdc_issued = FALSE; + Key *clientkey; + Ticket *t; + krb5_const_realm local_realm; + const PA_DATA *pac_options_data = NULL; + int pac_options_data_idx = 0; + krb5_boolean rbcd_support = FALSE; + + memset(&evidence_tkt, 0, sizeof(evidence_tkt)); + local_realm = + krb5_principal_get_comp_string(r->context, r->krbtgt->principal, 1); + + /* + * We require that the service's TGT has a PAC; this will have been + * validated prior to this function being called. + */ + if (r->pac == NULL) { + ret = KRB5KDC_ERR_BADOPTION; + kdc_audit_addreason((kdc_request_t)r, "Missing PAC"); + kdc_log(r->context, r->config, 4, + "Constrained delegation without PAC, %s/%s", + r->cname, r->sname); + goto out; + } + + t = &b->additional_tickets->val[0]; + + ret = _krb5_principalname2krb5_principal(r->context, + &s4u_server_name, + t->sname, + t->realm); + if (ret) + goto out; + + ret = krb5_unparse_name(r->context, s4u_server_name, &s4usname); + if (ret) + goto out; + + /* + * Look up the name given in the ticket in the database. We don’t ask for + * canonicalisation, so that we get back the same principal that was + * specified in the ticket. + */ + ret = _kdc_db_fetch(r->context, r->config, s4u_server_name, + HDB_F_GET_SERVER | HDB_F_DELAY_NEW_KEYS | flags, + NULL, &s4u_serverdb, &s4u_server); + if (ret == HDB_ERR_NOENTRY) + ret = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; + if (ret) { + kdc_audit_addreason((kdc_request_t)r, + "Constrained delegation service principal unknown"); + goto out; + } + + /* + * Check that the delegating server (r->client) is the same one as specified + * in the ticket. This is to make sure that the server hasn’t forged the + * sname, which is in the unencrypted part of the ticket. + */ + ret = _kdc_check_client_matches_target_service(r->context, + r->config, + s4u_serverdb, + s4u_server, + r->client, + r->client_princ); + if (ret == KRB5KRB_AP_ERR_BADMATCH) + ret = KRB5KDC_ERR_BADOPTION; + if (ret) + goto out; + + ret = hdb_enctype2key(r->context, r->client, + hdb_kvno2keys(r->context, r->client, + t->enc_part.kvno ? * t->enc_part.kvno : 0), + t->enc_part.etype, &clientkey); + if (ret) { + ret = KRB5KDC_ERR_ETYPE_NOSUPP; /* XXX */ + goto out; + } + + ret = krb5_decrypt_ticket(r->context, t, &clientkey->key, &evidence_tkt, 0); + if (ret) { + kdc_audit_addreason((kdc_request_t)r, + "Failed to decrypt constrained delegation ticket"); + kdc_log(r->context, r->config, 4, + "failed to decrypt ticket for " + "constrained delegation from %s to %s", r->cname, r->sname); + goto out; + } + + ret = _krb5_principalname2krb5_principal(r->context, + &s4u_client_name, + evidence_tkt.cname, + evidence_tkt.crealm); + if (ret) + goto out; + + ret = krb5_unparse_name(r->context, s4u_client_name, &s4ucname); + if (ret) + goto out; + + kdc_audit_addkv((kdc_request_t)r, 0, "impersonatee", "%s", s4ucname); + + /* check that ticket is valid */ + if (evidence_tkt.flags.forwardable == 0) { + kdc_audit_addreason((kdc_request_t)r, + "Missing forwardable flag on ticket for constrained delegation"); + kdc_log(r->context, r->config, 4, + "Missing forwardable flag on ticket for " + "constrained delegation from %s (%s) as %s to %s ", + r->cname, s4usname, s4ucname, r->sname); + ret = KRB5KDC_ERR_BADOPTION; + goto out; + } + + pac_options_data = _kdc_find_padata(&r->req, + &pac_options_data_idx, + KRB5_PADATA_PAC_OPTIONS); + if (pac_options_data != NULL) { + PA_PAC_OPTIONS pac_options; + size_t size = 0; + + ret = decode_PA_PAC_OPTIONS(pac_options_data->padata_value.data, + pac_options_data->padata_value.length, + &pac_options, + &size); + if (ret) { + goto out; + } + + if (size != pac_options_data->padata_value.length) { + free_PA_PAC_OPTIONS(&pac_options); + ret = KRB5KDC_ERR_BADOPTION; + goto out; + } + + rbcd_support = pac_options.flags.resource_based_constrained_delegation != 0; + + free_PA_PAC_OPTIONS(&pac_options); + } + + if (rbcd_support) { + ret = check_rbcd(r->context, r->config, r->clientdb, + s4u_client_name, + r->krbtgt, r->client, + r->armor_server, r->armor_client, + r->pac, r->armor_pac, + r->server); + } else { + ret = KRB5KDC_ERR_BADOPTION; + } + if (ret == KRB5KDC_ERR_BADOPTION) { + /* RBCD was denied or not supported; try constrained delegation. */ + ret = check_constrained_delegation(r->context, r->config, r->clientdb, + r->client, r->server, r->server_princ); + if (ret) { + kdc_audit_addreason((kdc_request_t)r, + "Constrained delegation not allowed"); + kdc_log(r->context, r->config, 4, + "constrained delegation from %s (%s) as %s to %s not allowed", + r->cname, s4usname, s4ucname, r->sname); + goto out; + } + } else if (ret) { + kdc_audit_addreason((kdc_request_t)r, + "Resource-based constrained delegation not allowed"); + kdc_log(r->context, r->config, 4, + "resource-based constrained delegation from %s (%s) as %s to %s not allowed", + r->cname, s4usname, s4ucname, r->sname); + goto out; + } + + ret = _kdc_verify_flags(r->context, r->config, &evidence_tkt, s4ucname); + if (ret) { + kdc_audit_addreason((kdc_request_t)r, + "Constrained delegation ticket expired or invalid"); + goto out; + } + + /* Try lookup the delegated client in DB */ + ret = _kdc_db_fetch_client(r->context, r->config, flags, + s4u_client_name, s4ucname, local_realm, + &s4u_clientdb, &s4u_client); + if (ret) + goto out; + + if (s4u_client != NULL) { + ret = kdc_check_flags(r, FALSE, s4u_client, r->server); + if (ret) + goto out; + } + + /* + * TODO: pass in t->sname and t->realm and build + * a S4U_DELEGATION_INFO blob to the PAC. + */ + ret = _kdc_check_pac(r, s4u_client_name, s4u_server, + s4u_client, r->server, r->krbtgt, r->client, + &clientkey->key, &r->ticket_key->key, &evidence_tkt, + &ad_kdc_issued, &s4u_pac, + &s4u_canon_client_name, &s4u_pac_attributes); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + kdc_audit_addreason((kdc_request_t)r, + "Constrained delegation ticket PAC check failed"); + kdc_log(r->context, r->config, 4, + "Verify delegated PAC failed to %s for client " + "%s (%s) as %s from %s with %s", + r->sname, r->cname, s4usname, s4ucname, r->from, msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + if (s4u_pac == NULL || !ad_kdc_issued) { + ret = KRB5KDC_ERR_BADOPTION; + kdc_log(r->context, r->config, 4, + "Ticket not signed with PAC; service %s failed for " + "for delegation to %s for client %s (%s) from %s; (%s).", + r->sname, s4ucname, s4usname, r->cname, r->from, + s4u_pac ? "Ticket unsigned" : "No PAC"); + kdc_audit_addreason((kdc_request_t)r, + "Constrained delegation ticket not signed"); + goto out; + } + + heim_assert(s4u_pac != NULL, "ad_kdc_issued implies the PAC is non-NULL"); + + ret = _kdc_pac_update(r, s4u_client_name, s4u_server, r->pac, + s4u_client, r->server, r->krbtgt, + &s4u_pac); + if (ret == KRB5_PLUGIN_NO_HANDLE) { + ret = 0; + } + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + kdc_audit_addreason((kdc_request_t)r, + "Constrained delegation ticket PAC update failed"); + kdc_log(r->context, r->config, 4, + "Update delegated PAC failed to %s for client " + "%s (%s) as %s from %s with %s", + r->sname, r->cname, s4usname, s4ucname, r->from, msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + /* + * If the evidence ticket PAC didn't include PAC_UPN_DNS_INFO with + * the canonical client name, but the user is local to our KDC, we + * can insert the canonical client name ourselves. + */ + if (s4u_canon_client_name == NULL && s4u_client != NULL) { + ret = krb5_copy_principal(r->context, s4u_client->principal, + &s4u_canon_client_name); + if (ret) + goto out; + } + + if (b->enc_authorization_data && r->rk_is_subkey == 0) { + krb5_free_keyblock_contents(r->context, &r->enc_ad_key); + ret = krb5_copy_keyblock_contents(r->context, + &evidence_tkt.key, + &r->enc_ad_key); + if (ret) + goto out; + } + + kdc_log(r->context, r->config, 4, "constrained delegation for %s " + "from %s (%s) to %s", s4ucname, r->cname, s4usname, r->sname); + + /* + * Replace all client information in the request with the + * impersonated client. (The audit entry containing the original + * client name will have been created before this point.) + */ + _kdc_request_set_cname_nocopy((kdc_request_t)r, &s4ucname); + _kdc_request_set_client_princ_nocopy(r, &s4u_client_name); + + _kdc_free_ent(r->context, r->clientdb, r->client); + r->client = s4u_client; + s4u_client = NULL; + r->clientdb = s4u_clientdb; + s4u_clientdb = NULL; + + _kdc_request_set_canon_client_princ_nocopy(r, &s4u_canon_client_name); + _kdc_request_set_pac_nocopy(r, &s4u_pac); + + r->pac_attributes = s4u_pac_attributes; + + r->et.authtime = evidence_tkt.authtime; + +out: + if (s4u_client) + _kdc_free_ent(r->context, s4u_clientdb, s4u_client); + if (s4u_server) + _kdc_free_ent(r->context, s4u_serverdb, s4u_server); + krb5_free_principal(r->context, s4u_client_name); + krb5_xfree(s4ucname); + krb5_free_principal(r->context, s4u_server_name); + krb5_xfree(s4usname); + krb5_free_principal(r->context, s4u_canon_client_name); + krb5_pac_free(r->context, s4u_pac); + + free_EncTicketPart(&evidence_tkt); + + return ret; +} diff --git a/third_party/heimdal/kdc/negotiate_token_validator.c b/third_party/heimdal/kdc/negotiate_token_validator.c new file mode 100644 index 0000000..20250c6 --- /dev/null +++ b/third_party/heimdal/kdc/negotiate_token_validator.c @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2019 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. + */ + +/* + * This is a plugin by which bx509d can validate Negotiate tokens. + * + * [kdc] + * negotiate_token_validator = { + * keytab = ... + * } + */ + +#define _DEFAULT_SOURCE +#define _BSD_SOURCE +#define _GNU_SOURCE 1 + +#include <sys/types.h> +#include <sys/stat.h> +#include <ctype.h> +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <base64.h> +#include <roken.h> +#include <heimbase.h> +#include <krb5.h> +#include <common_plugin.h> +#include <gssapi/gssapi.h> +#include <token_validator_plugin.h> + +static int +display_status(krb5_context context, + OM_uint32 major, + OM_uint32 minor, + gss_cred_id_t acred, + gss_ctx_id_t gctx, + gss_OID mech_type) +{ + gss_buffer_desc buf = GSS_C_EMPTY_BUFFER; + OM_uint32 dmaj, dmin; + OM_uint32 more = 0; + char *gmmsg = NULL; + char *gmsg = NULL; + char *s = NULL; + + do { + gss_release_buffer(&dmin, &buf); + dmaj = gss_display_status(&dmin, major, GSS_C_GSS_CODE, GSS_C_NO_OID, + &more, &buf); + if (GSS_ERROR(dmaj) || + buf.length >= INT_MAX || + asprintf(&s, "%s%s%.*s", gmsg ? gmsg : "", gmsg ? ": " : "", + (int)buf.length, (char *)buf.value) == -1 || + s == NULL) { + free(gmsg); + gmsg = NULL; + break; + } + gmsg = s; + s = NULL; + } while (!GSS_ERROR(dmaj) && more); + if (mech_type != GSS_C_NO_OID) { + do { + gss_release_buffer(&dmin, &buf); + dmaj = gss_display_status(&dmin, major, GSS_C_MECH_CODE, mech_type, + &more, &buf); + if (GSS_ERROR(dmaj) || + asprintf(&s, "%s%s%.*s", gmmsg ? gmmsg : "", gmmsg ? ": " : "", + (int)buf.length, (char *)buf.value) == -1 || + s == NULL) { + free(gmmsg); + gmmsg = NULL; + break; + } + gmmsg = s; + s = NULL; + } while (!GSS_ERROR(dmaj) && more); + } + if (gmsg == NULL) + krb5_set_error_message(context, ENOMEM, "Error displaying GSS-API " + "status"); + else + krb5_set_error_message(context, EACCES, "%s%s%s%s", gmmsg, + gmmsg ? " (" : "", gmmsg ? gmmsg : "", + gmmsg ? ")" : ""); + if (acred && gctx) + krb5_prepend_error_message(context, EACCES, "Failed to validate " + "Negotiate token due to error examining " + "GSS-API security context"); + else if (acred) + krb5_prepend_error_message(context, EACCES, "Failed to validate " + "Negotiate token due to error accepting " + "GSS-API security context token"); + else + krb5_prepend_error_message(context, EACCES, "Failed to validate " + "Negotiate token due to error acquiring " + "GSS-API default acceptor credential"); + return EACCES; +} + +static KRB5_LIB_CALL krb5_error_code +validate(void *ctx, + krb5_context context, + const char *realm, + const char *token_type, + krb5_data *token, + const char * const *audiences, + size_t naudiences, + krb5_boolean *result, + krb5_principal *actual_principal, + krb5_times *token_times) +{ + gss_buffer_desc adisplay_name = GSS_C_EMPTY_BUFFER; + gss_buffer_desc idisplay_name = GSS_C_EMPTY_BUFFER; + gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER; + gss_buffer_desc input_token; + gss_cred_id_t acred = GSS_C_NO_CREDENTIAL; + gss_ctx_id_t gctx = GSS_C_NO_CONTEXT; + gss_name_t aname = GSS_C_NO_NAME; + gss_name_t iname = GSS_C_NO_NAME; + gss_OID mech_type = GSS_C_NO_OID; + const char *kt = krb5_config_get_string(context, NULL, "kdc", + "negotiate_token_validator", + "keytab", NULL); + OM_uint32 major, minor, ret_flags, time_rec; + size_t i; + char *token_decoded = NULL; + void *token_copy = NULL; + char *princ_str = NULL; + int ret = 0; + + if (strcmp(token_type, "Negotiate") != 0) + return KRB5_PLUGIN_NO_HANDLE; + + if (kt) { + gss_key_value_element_desc store_keytab_kv; + gss_key_value_set_desc store; + gss_OID_desc mech_set[2] = { *GSS_KRB5_MECHANISM, *GSS_SPNEGO_MECHANISM }; + gss_OID_set_desc mechs = { 2, mech_set }; + + store_keytab_kv.key = "keytab"; + store_keytab_kv.value = kt; + store.elements = &store_keytab_kv; + store.count = 1; + major = gss_acquire_cred_from(&minor, GSS_C_NO_NAME, GSS_C_INDEFINITE, + &mechs, GSS_C_ACCEPT, &store, &acred, NULL, + NULL); + if (major != GSS_S_COMPLETE) + return display_status(context, major, minor, acred, gctx, mech_type); + + mechs.count = 1; + major = gss_set_neg_mechs(&minor, acred, &mechs); + if (major != GSS_S_COMPLETE) + return display_status(context, major, minor, acred, gctx, mech_type); + } /* else we'll use the default credential */ + + if ((token_decoded = malloc(token->length)) == NULL || + (token_copy = calloc(1, token->length + 1)) == NULL) + goto enomem; + + memcpy(token_copy, token->data, token->length); + if ((ret = rk_base64_decode(token_copy, token_decoded)) <= 0) { + krb5_set_error_message(context, EACCES, "Negotiate token malformed"); + ret = EACCES; + goto out; + } + + input_token.value = token_decoded; + input_token.length = ret; + major = gss_accept_sec_context(&minor, &gctx, acred, &input_token, NULL, + &iname, &mech_type, &output_token, + &ret_flags, &time_rec, NULL); + + if (mech_type == GSS_C_NO_OID || + !gss_oid_equal(mech_type, GSS_KRB5_MECHANISM)) { + krb5_set_error_message(context, ret = EACCES, "Negotiate token used " + "non-Kerberos mechanism"); + goto out; + } + + if (major != GSS_S_COMPLETE) { + ret = display_status(context, major, minor, acred, gctx, mech_type); + if (ret == 0) + ret = EINVAL; + goto out; + } + + major = gss_inquire_context(&minor, gctx, NULL, &aname, NULL, NULL, + NULL, NULL, NULL); + if (major == GSS_S_COMPLETE) + major = gss_display_name(&minor, aname, &adisplay_name, NULL); + if (major == GSS_S_COMPLETE) + major = gss_display_name(&minor, iname, &idisplay_name, NULL); + if (major != GSS_S_COMPLETE) { + ret = display_status(context, major, minor, acred, gctx, mech_type); + if (ret == 0) + ret = EINVAL; + goto out; + } + + for (i = 0; i < naudiences; i++) { + const char *s = adisplay_name.value; + size_t slen = adisplay_name.length; + size_t len = strlen(audiences[i]); + + if (slen >= sizeof("HTTP/") - 1 && + slen >= sizeof("HTTP/") - 1 + len && + memcmp(s, "HTTP/", sizeof("HTTP/") - 1) == 0 && + memcmp(s + sizeof("HTTP/") - 1, audiences[i], len) == 0 && + s[sizeof("HTTP/") - 1 + len] == '@') + break; + } + if (i == naudiences) { + /* This handles the case where naudiences == 0 as an error */ + krb5_set_error_message(context, EACCES, "Negotiate token used " + "wrong HTTP service host acceptor name"); + goto out; + } + + if ((princ_str = calloc(1, idisplay_name.length + 1)) == NULL) + goto enomem; + memcpy(princ_str, idisplay_name.value, idisplay_name.length); + if ((ret = krb5_parse_name(context, princ_str, actual_principal))) + goto out; + + /* XXX Need name attributes to get authtime/starttime/renew_till */ + token_times->authtime = 0; + token_times->starttime = time(NULL) - 300; + token_times->endtime = token_times->starttime + 300 + time_rec; + token_times->renew_till = 0; + + *result = TRUE; + goto out; + +enomem: + ret = krb5_enomem(context); +out: + gss_delete_sec_context(&minor, &gctx, NULL); + gss_release_buffer(&minor, &adisplay_name); + gss_release_buffer(&minor, &idisplay_name); + gss_release_buffer(&minor, &output_token); + gss_release_cred(&minor, &acred); + gss_release_name(&minor, &aname); + gss_release_name(&minor, &iname); + free(token_decoded); + free(token_copy); + free(princ_str); + return ret; +} + +static KRB5_LIB_CALL krb5_error_code +negotiate_init(krb5_context context, void **c) +{ + *c = NULL; + return 0; +} + +static KRB5_LIB_CALL void +negotiate_fini(void *c) +{ +} + +static krb5plugin_token_validator_ftable plug_desc = + { 1, negotiate_init, negotiate_fini, validate }; + +static krb5plugin_token_validator_ftable *plugs[] = { &plug_desc }; + +static uintptr_t +negotiate_get_instance(const char *libname) +{ + if (strcmp(libname, "krb5") == 0) + return krb5_get_instance(libname); + + return 0; +} + +krb5_plugin_load_ft kdc_token_validator_plugin_load; + +krb5_error_code KRB5_CALLCONV +kdc_token_validator_plugin_load(heim_pcontext context, + krb5_get_instance_func_t *get_instance, + size_t *num_plugins, + krb5_plugin_common_ftable_cp **plugins) +{ + *get_instance = negotiate_get_instance; + *num_plugins = sizeof(plugs) / sizeof(plugs[0]); + *plugins = (krb5_plugin_common_ftable_cp *)plugs; + return 0; +} diff --git a/third_party/heimdal/kdc/pkinit-ec.c b/third_party/heimdal/kdc/pkinit-ec.c new file mode 100644 index 0000000..31a5fe7 --- /dev/null +++ b/third_party/heimdal/kdc/pkinit-ec.c @@ -0,0 +1,548 @@ +/* + * Copyright (c) 2016 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2009 Apple Inc. 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. + */ + +#include <config.h> +#include <roken.h> + +#ifdef PKINIT + +/* + * As with the other *-ec.c files in Heimdal, this is a bit of a hack. + * + * The idea is to use OpenSSL for EC because hcrypto doesn't have the + * required functionality at this time. To do this we segregate + * EC-using code into separate source files and then we arrange for them + * to get the OpenSSL headers and not the conflicting hcrypto ones. + * + * Because of auto-generated *-private.h headers, we end up needing to + * make sure various types are defined before we include them, thus the + * strange header include order here. + */ + +#ifdef HAVE_HCRYPTO_W_OPENSSL +#include <openssl/evp.h> +#include <openssl/ec.h> +#include <openssl/ecdsa.h> +#include <openssl/rsa.h> +#include <openssl/bn.h> +#include <openssl/dh.h> +#include <openssl/objects.h> +#ifdef HAVE_OPENSSL_30 +#include <openssl/core_names.h> +#endif +#define HEIM_NO_CRYPTO_HDRS +#endif /* HAVE_HCRYPTO_W_OPENSSL */ + +#define NO_HCRYPTO_POLLUTION + +#include "kdc_locl.h" +#include <hcrypto/des.h> +#include <heim_asn1.h> +#include <rfc2459_asn1.h> +#include <cms_asn1.h> +#include <pkinit_asn1.h> + +#include <hx509.h> +#include "../lib/hx509/hx_locl.h" +#include <hx509-private.h> + +void +_kdc_pk_free_client_ec_param(krb5_context context, + void *k0, + void *k1) +{ +#ifdef HAVE_HCRYPTO_W_OPENSSL +#ifdef HAVE_OPENSSL_30 + EVP_PKEY_free(k0); + EVP_PKEY_free(k1); +#else + EC_KEY_free(k0); + EC_KEY_free(k1); +#endif +#endif +} + +#ifdef HAVE_HCRYPTO_W_OPENSSL +#ifdef HAVE_OPENSSL_30 +static krb5_error_code +generate_ecdh_keyblock_ossl30(krb5_context context, + EVP_PKEY *ec_key_pub, /* the client's public key */ + EVP_PKEY **ec_key_priv, /* the KDC's ephemeral private */ + unsigned char **dh_gen_key, /* shared secret */ + size_t *dh_gen_keylen) +{ + EVP_PKEY_CTX *pctx = NULL; + EVP_PKEY *ephemeral = NULL; + krb5_error_code ret = 0; + unsigned char *p = NULL; + size_t size = 0; + + if (ec_key_pub == NULL) + /* XXX This seems like an internal error that should be impossible */ + krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC, + "Missing client ECDH key agreement public key"); + if (ret == 0 && + (ephemeral = + EVP_EC_gen(OSSL_EC_curve_nid2name(NID_X9_62_prime256v1))) == NULL) + krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC, + "Could not generate an ECDH key agreement private key"); + if (ret == 0 && + (pctx = EVP_PKEY_CTX_new(ephemeral, NULL)) == NULL) + ret = krb5_enomem(context); + if (ret == 0 && EVP_PKEY_derive_init(pctx) != 1) + ret = krb5_enomem(context); + if (ret == 0 && + EVP_PKEY_CTX_set_ecdh_kdf_type(pctx, EVP_PKEY_ECDH_KDF_NONE) != 1) + krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC, + "Could not generate an ECDH key agreement private key " + "(EVP_PKEY_CTX_set_dh_kdf_type)"); + if (ret == 0 && + EVP_PKEY_derive_set_peer_ex(pctx, ec_key_pub, 1) != 1) + krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC, + "Could not generate an ECDH key agreement private key " + "(EVP_PKEY_derive_set_peer_ex)"); + if (ret == 0 && + (EVP_PKEY_derive(pctx, NULL, &size) != 1 || size == 0)) + krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC, + "Could not generate an ECDH key agreement private key " + "(EVP_PKEY_derive)"); + if (ret == 0 && (p = malloc(size)) == NULL) + ret = krb5_enomem(context); + if (ret == 0 && + (EVP_PKEY_derive(pctx, p, &size) != 1 || size == 0)) + krb5_set_error_message(context, ret = KRB5KRB_ERR_GENERIC, + "Could not generate an ECDH key agreement private key " + "(EVP_PKEY_derive)"); + + if (ret) { + EVP_PKEY_free(ephemeral); + ephemeral = NULL; + free(p); + p = NULL; + size = 0; + } + + *ec_key_priv = ephemeral; + *dh_gen_keylen = size; + *dh_gen_key = p; + + EVP_PKEY_CTX_free(pctx); + return ret; +} +#else + +/* The empty line above is intentional to work around an mkproto bug */ +static krb5_error_code +generate_ecdh_keyblock_ossl11(krb5_context context, + EC_KEY *ec_key_pk, /* the client's public key */ + EC_KEY **ec_key_key, /* the KDC's ephemeral private */ + unsigned char **dh_gen_key, /* shared secret */ + size_t *dh_gen_keylen) +{ + const EC_GROUP *group; + EC_KEY *ephemeral; + krb5_keyblock key; + krb5_error_code ret; + unsigned char *p; + size_t size; + int len; + + *dh_gen_key = NULL; + *dh_gen_keylen = 0; + *ec_key_key = NULL; + + memset(&key, 0, sizeof(key)); + + if (ec_key_pk == NULL) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, "public_key"); + return ret; + } + + group = EC_KEY_get0_group(ec_key_pk); + if (group == NULL) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, "failed to get the group of " + "the client's public key"); + return ret; + } + + ephemeral = EC_KEY_new(); + if (ephemeral == NULL) + return krb5_enomem(context); + + EC_KEY_set_group(ephemeral, group); + + if (EC_KEY_generate_key(ephemeral) != 1) { + EC_KEY_free(ephemeral); + return krb5_enomem(context); + } + + size = (EC_GROUP_get_degree(group) + 7) / 8; + p = malloc(size); + if (p == NULL) { + EC_KEY_free(ephemeral); + return krb5_enomem(context); + } + + len = ECDH_compute_key(p, size, + EC_KEY_get0_public_key(ec_key_pk), + ephemeral, NULL); + if (len <= 0) { + free(p); + EC_KEY_free(ephemeral); + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, "Failed to compute ECDH " + "public shared secret"); + return ret; + } + + *ec_key_key = ephemeral; + *dh_gen_key = p; + *dh_gen_keylen = len; + + return 0; +} +#endif +#endif /* HAVE_HCRYPTO_W_OPENSSL */ + +krb5_error_code +_kdc_generate_ecdh_keyblock(krb5_context context, + void *ec_key_pk, /* the client's public key */ + void **ec_key_key, /* the KDC's ephemeral private */ + unsigned char **dh_gen_key, /* shared secret */ + size_t *dh_gen_keylen) +{ +#ifdef HAVE_HCRYPTO_W_OPENSSL +#ifdef HAVE_OPENSSL_30 + return generate_ecdh_keyblock_ossl30(context, ec_key_pk, + (EVP_PKEY **)ec_key_key, + dh_gen_key, dh_gen_keylen); +#else + return generate_ecdh_keyblock_ossl11(context, ec_key_pk, + (EC_KEY **)ec_key_key, + dh_gen_key, dh_gen_keylen); +#endif +#else + return ENOTSUP; +#endif /* HAVE_HCRYPTO_W_OPENSSL */ +} + +#ifdef HAVE_HCRYPTO_W_OPENSSL +#ifdef HAVE_OPENSSL_30 +static krb5_error_code +get_ecdh_param_ossl30(krb5_context context, + krb5_kdc_configuration *config, + SubjectPublicKeyInfo *dh_key_info, + EVP_PKEY **out) +{ + EVP_PKEY_CTX *pctx = NULL; + EVP_PKEY *template = NULL; + EVP_PKEY *public = NULL; + OSSL_PARAM params[2]; + krb5_error_code ret = 0; + ECParameters ecp; + const unsigned char *p; + const char *curve_sn = NULL; + size_t len; + char *curve_sn_dup = NULL; + int groupnid = NID_undef; + + /* XXX Algorithm agility; XXX KRB5_BADMSGTYPE?? */ + + /* + * In order for d2i_PublicKey() to work we need to create a template key + * that has the curve parameters for the subjectPublicKey. + * + * Or maybe we could learn to use the OSSL_DECODER(3) API. But this works, + * at least until OpenSSL deprecates d2i_PublicKey() and forces us to use + * OSSL_DECODER(3). + */ + + memset(&ecp, 0, sizeof(ecp)); + + if (dh_key_info->algorithm.parameters == NULL) + krb5_set_error_message(context, ret = KRB5_BADMSGTYPE, + "PKINIT missing algorithm parameter " + "in clientPublicValue"); + if (ret == 0) + ret = decode_ECParameters(dh_key_info->algorithm.parameters->data, + dh_key_info->algorithm.parameters->length, + &ecp, &len); + if (ret == 0 && ecp.element != choice_ECParameters_namedCurve) + krb5_set_error_message(context, ret = KRB5_BADMSGTYPE, + "PKINIT client used an unnamed curve"); + if (ret == 0 && + (groupnid = _hx509_ossl_oid2nid(&ecp.u.namedCurve)) == NID_undef) + krb5_set_error_message(context, ret = KRB5_BADMSGTYPE, + "PKINIT client used an unsupported curve"); + if (ret == 0 && (curve_sn = OBJ_nid2sn(groupnid)) == NULL) + krb5_set_error_message(context, ret = KRB5_BADMSGTYPE, + "Could not resolve curve NID %d to its short name", + groupnid); + if (ret == 0 && (curve_sn_dup = strdup(curve_sn)) == NULL) + ret = krb5_enomem(context); + if (ret == 0) { + if (der_heim_oid_cmp(&ecp.u.namedCurve, &asn1_oid_id_ec_group_secp256r1) != 0) + krb5_set_error_message(context, ret = KRB5_BADMSGTYPE, + "PKINIT client used an unsupported curve"); + } + if (ret == 0) { + /* + * Apparently there's no error checking to be done here? Why does + * OSSL_PARAM_construct_utf8_string() want a non-const for the value? + * Is that a bug in OpenSSL? + */ + params[0] = OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, + curve_sn_dup, 0); + params[1] = OSSL_PARAM_construct_end(); + + if ((pctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL)) == NULL) + ret = krb5_enomem(context); + } + if (ret == 0 && EVP_PKEY_fromdata_init(pctx) != 1) + ret = krb5_enomem(context); + if (ret == 0 && + EVP_PKEY_fromdata(pctx, &template, OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS, + params) != 1) + krb5_set_error_message(context, ret = KRB5_BADMSGTYPE, + "Could not set up to parse key for curve %s", + curve_sn); + + p = dh_key_info->subjectPublicKey.data; + len = dh_key_info->subjectPublicKey.length / 8; + if (ret == 0 && + (public = d2i_PublicKey(EVP_PKEY_EC, &template, &p, len)) == NULL) + krb5_set_error_message(context, ret = KRB5_BADMSGTYPE, + "Could not decode PKINIT client ECDH key"); + + if (ret) { + EVP_PKEY_free(public); + public = NULL; + } + + *out = public; + + /* FYI the EVP_PKEY_CTX takes ownership of the `template' key */ + EVP_PKEY_CTX_free(pctx); + free_ECParameters(&ecp); + free(curve_sn_dup); + return ret; +} +#else + +static krb5_error_code +get_ecdh_param_ossl11(krb5_context context, + krb5_kdc_configuration *config, + SubjectPublicKeyInfo *dh_key_info, + EC_KEY **out) +{ + ECParameters ecp; + EC_KEY *public = NULL; + krb5_error_code ret; + const unsigned char *p; + size_t len; + int nid; + + if (dh_key_info->algorithm.parameters == NULL) { + krb5_set_error_message(context, KRB5_BADMSGTYPE, + "PKINIT missing algorithm parameter " + "in clientPublicValue"); + return KRB5_BADMSGTYPE; + } + /* XXX Algorithm agility; XXX KRB5_BADMSGTYPE?? */ + + memset(&ecp, 0, sizeof(ecp)); + + ret = decode_ECParameters(dh_key_info->algorithm.parameters->data, + dh_key_info->algorithm.parameters->length, &ecp, &len); + if (ret) + goto out; + + if (ecp.element != choice_ECParameters_namedCurve) { + ret = KRB5_BADMSGTYPE; + goto out; + } + + if (der_heim_oid_cmp(&ecp.u.namedCurve, &asn1_oid_id_ec_group_secp256r1) == 0) + nid = NID_X9_62_prime256v1; + else { + ret = KRB5_BADMSGTYPE; + goto out; + } + + /* XXX verify group is ok */ + + public = EC_KEY_new_by_curve_name(nid); + + p = dh_key_info->subjectPublicKey.data; + len = dh_key_info->subjectPublicKey.length / 8; + if (o2i_ECPublicKey(&public, &p, len) == NULL) { + ret = KRB5_BADMSGTYPE; + krb5_set_error_message(context, ret, + "PKINIT failed to decode ECDH key"); + goto out; + } + *out = public; + public = NULL; + + out: + if (public) + EC_KEY_free(public); + free_ECParameters(&ecp); + return ret; +} +#endif +#endif /* HAVE_HCRYPTO_W_OPENSSL */ + +krb5_error_code +_kdc_get_ecdh_param(krb5_context context, + krb5_kdc_configuration *config, + SubjectPublicKeyInfo *dh_key_info, + void **out) +{ +#ifdef HAVE_HCRYPTO_W_OPENSSL +#ifdef HAVE_OPENSSL_30 + return get_ecdh_param_ossl30(context, config, dh_key_info, (EVP_PKEY **)out); +#else + return get_ecdh_param_ossl11(context, config, dh_key_info, (EC_KEY **)out); +#endif +#else + return ENOTSUP; +#endif /* HAVE_HCRYPTO_W_OPENSSL */ +} + + +/* + * + */ + +#ifdef HAVE_HCRYPTO_W_OPENSSL +#ifdef HAVE_OPENSSL_30 +static krb5_error_code +serialize_ecdh_key_ossl30(krb5_context context, + EVP_PKEY *key, + unsigned char **out, + size_t *out_len) +{ + unsigned char *p; + int len; + + *out = NULL; + *out_len = 0; + + len = i2d_PublicKey(key, NULL); + if (len <= 0) { + krb5_set_error_message(context, EOVERFLOW, + "PKINIT failed to encode ECDH key"); + return EOVERFLOW; + } + + *out = malloc(len); + if (*out == NULL) + return krb5_enomem(context); + + p = *out; + len = i2d_PublicKey(key, &p); + if (len <= 0) { + free(*out); + *out = NULL; + krb5_set_error_message(context, EINVAL /* XXX Better error please */, + "PKINIT failed to encode ECDH key"); + return EINVAL; + } + + *out_len = len * 8; + return 0; +} +#else + +static krb5_error_code +serialize_ecdh_key_ossl11(krb5_context context, + EC_KEY *key, + unsigned char **out, + size_t *out_len) +{ + unsigned char *p; + int len; + + *out = NULL; + *out_len = 0; + + len = i2o_ECPublicKey(key, NULL); + if (len <= 0) { + krb5_set_error_message(context, EOVERFLOW, + "PKINIT failed to encode ECDH key"); + return EOVERFLOW; + } + + *out = malloc(len); + if (*out == NULL) + return krb5_enomem(context); + + p = *out; + len = i2o_ECPublicKey(key, &p); + if (len <= 0) { + free(*out); + *out = NULL; + krb5_set_error_message(context, EINVAL /* XXX Better error please */, + "PKINIT failed to encode ECDH key"); + return EINVAL; + } + + *out_len = len * 8; + return 0; +} +#endif +#endif + +krb5_error_code +_kdc_serialize_ecdh_key(krb5_context context, + void *key, + unsigned char **out, + size_t *out_len) +{ +#ifdef HAVE_HCRYPTO_W_OPENSSL +#ifdef HAVE_OPENSSL_30 + return serialize_ecdh_key_ossl30(context, key, out, out_len); +#else + return serialize_ecdh_key_ossl11(context, key, out, out_len); +#endif +#else + return ENOTSUP; +#endif +} + +#endif diff --git a/third_party/heimdal/kdc/pkinit.c b/third_party/heimdal/kdc/pkinit.c new file mode 100644 index 0000000..c853359 --- /dev/null +++ b/third_party/heimdal/kdc/pkinit.c @@ -0,0 +1,2223 @@ +/* + * Copyright (c) 2003 - 2016 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2009 Apple Inc. 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. + */ + +#include "kdc_locl.h" + +#ifdef PKINIT + +#include <heim_asn1.h> +#include <rfc2459_asn1.h> +#include <cms_asn1.h> +#include <pkinit_asn1.h> + +#include <hx509.h> +#include "crypto-headers.h" + +struct pk_client_params { + enum krb5_pk_type type; + enum keyex_enum keyex; + union { + struct { + BIGNUM *public_key; + DH *key; + } dh; + struct { + void *public_key; + void *key; + } ecdh; + } u; + hx509_cert cert; + krb5_timestamp endtime; + krb5_timestamp max_life; + unsigned nonce; + EncryptionKey reply_key; + char *dh_group_name; + hx509_peer_info peer; + hx509_certs client_anchors; + hx509_verify_ctx verify_ctx; + heim_octet_string *freshness_token; +}; + +struct pk_principal_mapping { + unsigned int len; + struct pk_allowed_princ { + krb5_principal principal; + char *subject; + } *val; +}; + +static struct krb5_pk_identity *kdc_identity; +static struct pk_principal_mapping principal_mappings; +static struct krb5_dh_moduli **moduli; + +static struct { + krb5_data data; + time_t expire; + time_t next_update; +} ocsp; + +/* + * + */ + +static krb5_error_code +pk_check_pkauthenticator_win2k(krb5_context context, + PKAuthenticator_Win2k *a, + const KDC_REQ *req) +{ + krb5_timestamp now; + + krb5_timeofday (context, &now); + + /* XXX cusec */ + if (a->ctime == 0 || labs(a->ctime - now) > context->max_skew) { + krb5_clear_error_message(context); + return KRB5KRB_AP_ERR_SKEW; + } + return 0; +} + +static krb5_error_code +pk_check_pkauthenticator(krb5_context context, + const PKAuthenticator *a, + const KDC_REQ *req) +{ + krb5_error_code ret; + krb5_timestamp now; + Checksum checksum; + + krb5_timeofday (context, &now); + + /* XXX cusec */ + if (a->ctime == 0 || labs(a->ctime - now) > context->max_skew) { + krb5_clear_error_message(context); + return KRB5KRB_AP_ERR_SKEW; + } + + ret = krb5_create_checksum(context, + NULL, + 0, + CKSUMTYPE_SHA1, + req->req_body._save.data, + req->req_body._save.length, + &checksum); + if (ret) { + krb5_clear_error_message(context); + return ret; + } + + if (a->paChecksum == NULL) { + krb5_clear_error_message(context); + ret = KRB5_KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED; + goto out; + } + + if (der_heim_octet_string_cmp(a->paChecksum, &checksum.checksum) != 0) { + krb5_clear_error_message(context); + ret = KRB5KRB_ERR_GENERIC; + } + +out: + free_Checksum(&checksum); + + return ret; +} + +void +_kdc_pk_free_client_param(krb5_context context, pk_client_params *cp) +{ + if (cp == NULL) + return; + if (cp->cert) + hx509_cert_free(cp->cert); + if (cp->verify_ctx) + hx509_verify_destroy_ctx(cp->verify_ctx); + if (cp->keyex == USE_DH) { + if (cp->u.dh.key) + DH_free(cp->u.dh.key); + if (cp->u.dh.public_key) + BN_free(cp->u.dh.public_key); + } + if (cp->keyex == USE_ECDH) + _kdc_pk_free_client_ec_param(context, cp->u.ecdh.key, + cp->u.ecdh.public_key); + krb5_free_keyblock_contents(context, &cp->reply_key); + if (cp->dh_group_name) + free(cp->dh_group_name); + if (cp->peer) + hx509_peer_info_free(cp->peer); + if (cp->client_anchors) + hx509_certs_free(&cp->client_anchors); + if (cp->freshness_token) + der_free_octet_string(cp->freshness_token); + free(cp->freshness_token); + memset(cp, 0, sizeof(*cp)); + free(cp); +} + +static krb5_error_code +generate_dh_keyblock(krb5_context context, + pk_client_params *client_params, + krb5_enctype enctype) +{ + unsigned char *dh_gen_key = NULL; + krb5_keyblock key; + krb5_error_code ret; + size_t dh_gen_keylen, size; + + memset(&key, 0, sizeof(key)); + + if (client_params->keyex == USE_DH) { + + if (client_params->u.dh.public_key == NULL) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, "missing DH public_key"); + goto out; + } + + if (!DH_generate_key(client_params->u.dh.key)) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, + "Can't generate Diffie-Hellman keys"); + goto out; + } + + size = DH_size(client_params->u.dh.key); + + dh_gen_key = malloc(size); + if (dh_gen_key == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "malloc: out of memory"); + goto out; + } + + dh_gen_keylen = DH_compute_key(dh_gen_key,client_params->u.dh.public_key, client_params->u.dh.key); + if (dh_gen_keylen == (size_t)-1) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, + "Can't compute Diffie-Hellman key"); + goto out; + } + if (dh_gen_keylen < size) { + size -= dh_gen_keylen; + memmove(dh_gen_key + size, dh_gen_key, dh_gen_keylen); + memset(dh_gen_key, 0, size); + dh_gen_keylen += size; + } + } else if (client_params->keyex == USE_ECDH) { + if (client_params->u.ecdh.public_key == NULL) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, "missing ECDH public_key"); + goto out; + } + ret = _kdc_generate_ecdh_keyblock(context, + client_params->u.ecdh.public_key, + &client_params->u.ecdh.key, + &dh_gen_key, &dh_gen_keylen); + if (ret) + goto out; + } else { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, + "Diffie-Hellman not selected keys"); + goto out; + } + + ret = _krb5_pk_octetstring2key(context, + enctype, + dh_gen_key, dh_gen_keylen, + NULL, NULL, + &client_params->reply_key); + + out: + if (dh_gen_key) + free(dh_gen_key); + if (key.keyvalue.data) + krb5_free_keyblock_contents(context, &key); + + return ret; +} + +static BIGNUM * +integer_to_BN(krb5_context context, const char *field, heim_integer *f) +{ + BIGNUM *bn; + + bn = BN_bin2bn((const unsigned char *)f->data, f->length, NULL); + if (bn == NULL) { + krb5_set_error_message(context, KRB5_BADMSGTYPE, + "PKINIT: parsing BN failed %s", field); + return NULL; + } + BN_set_negative(bn, f->negative); + return bn; +} + +static krb5_error_code +get_dh_param(krb5_context context, + krb5_kdc_configuration *config, + SubjectPublicKeyInfo *dh_key_info, + pk_client_params *client_params) +{ + DomainParameters dhparam; + DH *dh = NULL; + krb5_error_code ret; + + memset(&dhparam, 0, sizeof(dhparam)); + + if ((dh_key_info->subjectPublicKey.length % 8) != 0) { + ret = KRB5_BADMSGTYPE; + krb5_set_error_message(context, ret, + "PKINIT: subjectPublicKey not aligned " + "to 8 bit boundary"); + goto out; + } + + if (dh_key_info->algorithm.parameters == NULL) { + krb5_set_error_message(context, KRB5_BADMSGTYPE, + "PKINIT missing algorithm parameter " + "in clientPublicValue"); + return KRB5_BADMSGTYPE; + } + + ret = decode_DomainParameters(dh_key_info->algorithm.parameters->data, + dh_key_info->algorithm.parameters->length, + &dhparam, + NULL); + if (ret) { + krb5_set_error_message(context, ret, "Can't decode algorithm " + "parameters in clientPublicValue"); + goto out; + } + + ret = _krb5_dh_group_ok(context, config->pkinit_dh_min_bits, + &dhparam.p, &dhparam.g, dhparam.q, moduli, + &client_params->dh_group_name); + if (ret) { + /* XXX send back proposal of better group */ + goto out; + } + + dh = DH_new(); + if (dh == NULL) { + ret = ENOMEM; + krb5_set_error_message(context, ret, "Cannot create DH structure"); + goto out; + } + ret = KRB5_BADMSGTYPE; + dh->p = integer_to_BN(context, "DH prime", &dhparam.p); + if (dh->p == NULL) + goto out; + dh->g = integer_to_BN(context, "DH base", &dhparam.g); + if (dh->g == NULL) + goto out; + + if (dhparam.q) { + dh->q = integer_to_BN(context, "DH p-1 factor", dhparam.q); + if (dh->q == NULL) + goto out; + } + + { + heim_integer glue; + size_t size; + + ret = decode_DHPublicKey(dh_key_info->subjectPublicKey.data, + dh_key_info->subjectPublicKey.length / 8, + &glue, + &size); + if (ret) { + krb5_clear_error_message(context); + return ret; + } + + client_params->u.dh.public_key = integer_to_BN(context, + "subjectPublicKey", + &glue); + der_free_heim_integer(&glue); + if (client_params->u.dh.public_key == NULL) { + ret = KRB5_BADMSGTYPE; + goto out; + } + } + + client_params->u.dh.key = dh; + dh = NULL; + ret = 0; + + out: + if (dh) + DH_free(dh); + free_DomainParameters(&dhparam); + return ret; +} + +krb5_error_code +_kdc_pk_rd_padata(astgs_request_t priv, + const PA_DATA *pa, + pk_client_params **ret_params) +{ + /* XXXrcd: we use priv vs r due to a conflict */ + krb5_context context = priv->context; + krb5_kdc_configuration *config = priv->config; + const KDC_REQ *req = &priv->req; + hdb_entry *client = priv->client; + pk_client_params *cp; + krb5_error_code ret; + heim_oid eContentType = { 0, NULL }, contentInfoOid = { 0, NULL }; + krb5_data eContent = { 0, NULL }; + krb5_data signed_content = { 0, NULL }; + const char *type = "unknown type"; + hx509_certs trust_anchors; + int have_data = 0; + const HDB_Ext_PKINIT_cert *pc; + + *ret_params = NULL; + + if (!config->enable_pkinit) { + kdc_log(context, config, 0, "PKINIT request but PKINIT not enabled"); + krb5_clear_error_message(context); + return 0; + } + + cp = calloc(1, sizeof(*cp)); + if (cp == NULL) { + krb5_clear_error_message(context); + ret = ENOMEM; + goto out; + } + + ret = hx509_certs_init(context->hx509ctx, + "MEMORY:trust-anchors", + 0, NULL, &trust_anchors); + if (ret) { + krb5_set_error_message(context, ret, "failed to create trust anchors"); + goto out; + } + + ret = hx509_certs_merge(context->hx509ctx, trust_anchors, + kdc_identity->anchors); + if (ret) { + hx509_certs_free(&trust_anchors); + krb5_set_error_message(context, ret, "failed to create verify context"); + goto out; + } + + /* Add any registered certificates for this client as trust anchors */ + ret = hdb_entry_get_pkinit_cert(client, &pc); + if (ret == 0 && pc != NULL) { + hx509_cert cert; + unsigned int i; + + for (i = 0; i < pc->len; i++) { + cert = hx509_cert_init_data(context->hx509ctx, + pc->val[i].cert.data, + pc->val[i].cert.length, + NULL); + if (cert == NULL) + continue; + hx509_certs_add(context->hx509ctx, trust_anchors, cert); + hx509_cert_free(cert); + } + } + + ret = hx509_verify_init_ctx(context->hx509ctx, &cp->verify_ctx); + if (ret) { + hx509_certs_free(&trust_anchors); + krb5_set_error_message(context, ret, "failed to create verify context"); + goto out; + } + + hx509_verify_set_time(cp->verify_ctx, kdc_time); + hx509_verify_attach_anchors(cp->verify_ctx, trust_anchors); + hx509_certs_free(&trust_anchors); + + hx509_verify_attach_revoke(cp->verify_ctx, kdc_identity->revokectx); + + if (config->pkinit_allow_proxy_certs) + hx509_verify_set_proxy_certificate(cp->verify_ctx, 1); + + if (pa->padata_type == KRB5_PADATA_PK_AS_REQ_WIN) { + PA_PK_AS_REQ_Win2k r; + + type = "PK-INIT-Win2k"; + + if (_kdc_is_anonymous(context, client->principal)) { + ret = KRB5_KDC_ERR_PUBLIC_KEY_ENCRYPTION_NOT_SUPPORTED; + krb5_set_error_message(context, ret, + "Anonymous client not supported in RSA mode"); + goto out; + } + + ret = decode_PA_PK_AS_REQ_Win2k(pa->padata_value.data, + pa->padata_value.length, + &r, + NULL); + if (ret) { + krb5_set_error_message(context, ret, "Can't decode " + "PK-AS-REQ-Win2k: %d", ret); + goto out; + } + + ret = hx509_cms_unwrap_ContentInfo(&r.signed_auth_pack, + &contentInfoOid, + &signed_content, + &have_data); + free_PA_PK_AS_REQ_Win2k(&r); + if (ret) { + krb5_set_error_message(context, ret, + "Can't unwrap ContentInfo(win): %d", ret); + goto out; + } + + } else if (pa->padata_type == KRB5_PADATA_PK_AS_REQ) { + PA_PK_AS_REQ r; + + type = "PK-INIT-IETF"; + + ret = decode_PA_PK_AS_REQ(pa->padata_value.data, + pa->padata_value.length, + &r, + NULL); + if (ret) { + krb5_set_error_message(context, ret, + "Can't decode PK-AS-REQ: %d", ret); + goto out; + } + + /* XXX look at r.kdcPkId */ + if (r.trustedCertifiers) { + ExternalPrincipalIdentifiers *edi = r.trustedCertifiers; + unsigned int i, maxedi; + + ret = hx509_certs_init(context->hx509ctx, + "MEMORY:client-anchors", + 0, NULL, + &cp->client_anchors); + if (ret) { + krb5_set_error_message(context, ret, + "Can't allocate client anchors: %d", + ret); + goto out; + + } + /* + * If the client sent more than 10 EDIs, don't bother + * looking at more than 10 for performance reasons. + */ + maxedi = edi->len; + if (maxedi > 10) + maxedi = 10; + for (i = 0; i < maxedi; i++) { + IssuerAndSerialNumber iasn; + hx509_query *q; + hx509_cert cert; + size_t size; + + if (edi->val[i].issuerAndSerialNumber == NULL) + continue; + + ret = hx509_query_alloc(context->hx509ctx, &q); + if (ret) { + krb5_set_error_message(context, ret, + "Failed to allocate hx509_query"); + goto out; + } + + ret = decode_IssuerAndSerialNumber(edi->val[i].issuerAndSerialNumber->data, + edi->val[i].issuerAndSerialNumber->length, + &iasn, + &size); + if (ret) { + hx509_query_free(context->hx509ctx, q); + continue; + } + ret = hx509_query_match_issuer_serial(q, &iasn.issuer, &iasn.serialNumber); + free_IssuerAndSerialNumber(&iasn); + if (ret) { + hx509_query_free(context->hx509ctx, q); + continue; + } + + ret = hx509_certs_find(context->hx509ctx, + kdc_identity->certs, + q, + &cert); + hx509_query_free(context->hx509ctx, q); + if (ret) + continue; + hx509_certs_add(context->hx509ctx, + cp->client_anchors, cert); + hx509_cert_free(cert); + } + } + + ret = hx509_cms_unwrap_ContentInfo(&r.signedAuthPack, + &contentInfoOid, + &signed_content, + &have_data); + free_PA_PK_AS_REQ(&r); + if (ret) { + krb5_set_error_message(context, ret, + "Can't unwrap ContentInfo: %d", ret); + goto out; + } + + } else { + krb5_clear_error_message(context); + ret = KRB5KDC_ERR_PADATA_TYPE_NOSUPP; + goto out; + } + + ret = der_heim_oid_cmp(&contentInfoOid, &asn1_oid_id_pkcs7_signedData); + if (ret != 0) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, + "PK-AS-REQ-Win2k invalid content type oid"); + goto out; + } + + if (!have_data) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, + "PK-AS-REQ-Win2k no signed auth pack"); + goto out; + } + + { + hx509_certs signer_certs; + int flags = HX509_CMS_VS_ALLOW_DATA_OID_MISMATCH; /* BTMM */ + + if (_kdc_is_anonymous(context, client->principal) + || (config->historical_anon_realm && _kdc_is_anon_request(req))) + flags |= HX509_CMS_VS_ALLOW_ZERO_SIGNER; + + ret = hx509_cms_verify_signed(context->hx509ctx, + cp->verify_ctx, + flags, + signed_content.data, + signed_content.length, + NULL, + kdc_identity->certpool, + &eContentType, + &eContent, + &signer_certs); + if (ret) { + char *s = hx509_get_error_string(context->hx509ctx, ret); + krb5_warnx(context, "PKINIT: failed to verify signature: %s: %d", + s, ret); + free(s); + goto out; + } + + if (signer_certs) { + ret = hx509_get_one_cert(context->hx509ctx, signer_certs, + &cp->cert); + hx509_certs_free(&signer_certs); + } + if (ret) + goto out; + } + + /* Signature is correct, now verify the signed message */ + if (der_heim_oid_cmp(&eContentType, &asn1_oid_id_pkcs7_data) != 0 && + der_heim_oid_cmp(&eContentType, &asn1_oid_id_pkauthdata) != 0) + { + ret = KRB5_BADMSGTYPE; + krb5_set_error_message(context, ret, "got wrong oid for PK AuthData"); + goto out; + } + + if (pa->padata_type == KRB5_PADATA_PK_AS_REQ_WIN) { + AuthPack_Win2k ap; + + ret = decode_AuthPack_Win2k(eContent.data, + eContent.length, + &ap, + NULL); + if (ret) { + krb5_set_error_message(context, ret, + "Can't decode AuthPack: %d", ret); + goto out; + } + + ret = pk_check_pkauthenticator_win2k(context, + &ap.pkAuthenticator, + req); + if (ret) { + free_AuthPack_Win2k(&ap); + goto out; + } + + cp->type = PKINIT_WIN2K; + cp->nonce = ap.pkAuthenticator.nonce; + + if (ap.clientPublicValue) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(context, ret, + "DH not supported for Win2k"); + free_AuthPack_Win2k(&ap); + goto out; + } + free_AuthPack_Win2k(&ap); + + } else if (pa->padata_type == KRB5_PADATA_PK_AS_REQ) { + AuthPack ap; + + ret = decode_AuthPack(eContent.data, + eContent.length, + &ap, + NULL); + if (ret) { + krb5_set_error_message(context, ret, + "Can't decode AuthPack: %d", ret); + free_AuthPack(&ap); + goto out; + } + + if (_kdc_is_anonymous(context, client->principal) && + ap.clientPublicValue == NULL) { + free_AuthPack(&ap); + ret = KRB5_KDC_ERR_PUBLIC_KEY_ENCRYPTION_NOT_SUPPORTED; + krb5_set_error_message(context, ret, + "Anonymous client not supported in RSA mode"); + goto out; + } + + ret = pk_check_pkauthenticator(context, + &ap.pkAuthenticator, + req); + if (ret) { + free_AuthPack(&ap); + goto out; + } + + cp->type = PKINIT_27; + cp->nonce = ap.pkAuthenticator.nonce; + + if (ap.clientPublicValue) { + if (der_heim_oid_cmp(&ap.clientPublicValue->algorithm.algorithm, &asn1_oid_id_dhpublicnumber) == 0) { + cp->keyex = USE_DH; + ret = get_dh_param(context, config, + ap.clientPublicValue, cp); + } else if (der_heim_oid_cmp(&ap.clientPublicValue->algorithm.algorithm, &asn1_oid_id_ecPublicKey) == 0) { + cp->keyex = USE_ECDH; + ret = _kdc_get_ecdh_param(context, config, + ap.clientPublicValue, + &cp->u.ecdh.public_key); + } else { + ret = KRB5_BADMSGTYPE; + krb5_set_error_message(context, ret, + "PKINIT unknown DH mechanism"); + } + if (ret) { + free_AuthPack(&ap); + goto out; + } + } else + cp->keyex = USE_RSA; + + ret = hx509_peer_info_alloc(context->hx509ctx, + &cp->peer); + if (ret) { + free_AuthPack(&ap); + goto out; + } + + if (ap.supportedCMSTypes) { + ret = hx509_peer_info_set_cms_algs(context->hx509ctx, + cp->peer, + ap.supportedCMSTypes->val, + ap.supportedCMSTypes->len); + if (ret) { + free_AuthPack(&ap); + goto out; + } + } else { + /* assume old client */ + hx509_peer_info_add_cms_alg(context->hx509ctx, cp->peer, + hx509_crypto_des_rsdi_ede3_cbc()); + hx509_peer_info_add_cms_alg(context->hx509ctx, cp->peer, + hx509_signature_rsa_with_sha1()); + hx509_peer_info_add_cms_alg(context->hx509ctx, cp->peer, + hx509_signature_sha1()); + } + + /* + * Copy the freshness token into the out parameters if it is present. + */ + if (ap.pkAuthenticator.freshnessToken != NULL) { + cp->freshness_token = calloc(1, sizeof (*cp->freshness_token)); + if (cp->freshness_token == NULL) { + ret = ENOMEM; + free_AuthPack(&ap); + goto out; + } + + ret = der_copy_octet_string(ap.pkAuthenticator.freshnessToken, cp->freshness_token); + if (ret) { + free_AuthPack(&ap); + goto out; + } + } + + free_AuthPack(&ap); + } else + krb5_abortx(context, "internal pkinit error"); + + kdc_log(context, config, 0, "PKINIT request of type %s", type); + +out: + if (ret) + krb5_warn(context, ret, "PKINIT"); + + if (signed_content.data) + free(signed_content.data); + krb5_data_free(&eContent); + der_free_oid(&eContentType); + der_free_oid(&contentInfoOid); + if (ret) { + _kdc_pk_free_client_param(context, cp); + } else + *ret_params = cp; + return ret; +} + +krb5_timestamp +_kdc_pk_endtime(pk_client_params *pkp) +{ + return pkp->endtime; +} + +krb5_timestamp +_kdc_pk_max_life(pk_client_params *pkp) +{ + return pkp->max_life; +} + +unsigned +_kdc_pk_nonce(pk_client_params *pkp) +{ + return pkp->nonce; +} + +/* + * + */ + +static krb5_error_code +BN_to_integer(krb5_context context, BIGNUM *bn, heim_integer *integer) +{ + integer->length = BN_num_bytes(bn); + integer->data = malloc(integer->length); + if (integer->data == NULL) { + krb5_clear_error_message(context); + return ENOMEM; + } + BN_bn2bin(bn, integer->data); + integer->negative = BN_is_negative(bn); + return 0; +} + +static krb5_error_code +pk_mk_pa_reply_enckey(krb5_context context, + krb5_kdc_configuration *config, + pk_client_params *cp, + const KDC_REQ *req, + const krb5_data *req_buffer, + krb5_keyblock *reply_key, + ContentInfo *content_info, + hx509_cert *kdc_cert) +{ + const heim_oid *envelopedAlg = NULL, *sdAlg = NULL, *evAlg = NULL; + krb5_error_code ret; + krb5_data buf, signed_data; + size_t size = 0; + int do_win2k = 0; + + krb5_data_zero(&buf); + krb5_data_zero(&signed_data); + + *kdc_cert = NULL; + + /* + * If the message client is a win2k-type but it sends pa data + * 09-binding it expects a IETF (checksum) reply so there can be + * no replay attacks. + */ + + switch (cp->type) { + case PKINIT_WIN2K: { + int i = 0; + if (_kdc_find_padata(req, &i, KRB5_PADATA_PK_AS_09_BINDING) == NULL + && config->pkinit_require_binding == 0) + { + do_win2k = 1; + } + sdAlg = &asn1_oid_id_pkcs7_data; + evAlg = &asn1_oid_id_pkcs7_data; + envelopedAlg = &asn1_oid_id_rsadsi_des_ede3_cbc; + break; + } + case PKINIT_27: + sdAlg = &asn1_oid_id_pkrkeydata; + evAlg = &asn1_oid_id_pkcs7_signedData; + break; + default: + krb5_abortx(context, "internal pkinit error"); + } + + if (do_win2k) { + ReplyKeyPack_Win2k kp; + memset(&kp, 0, sizeof(kp)); + + ret = copy_EncryptionKey(reply_key, &kp.replyKey); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + kp.nonce = cp->nonce; + + ASN1_MALLOC_ENCODE(ReplyKeyPack_Win2k, + buf.data, buf.length, + &kp, &size,ret); + free_ReplyKeyPack_Win2k(&kp); + } else { + krb5_crypto ascrypto; + ReplyKeyPack kp; + memset(&kp, 0, sizeof(kp)); + + ret = copy_EncryptionKey(reply_key, &kp.replyKey); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + ret = krb5_crypto_init(context, reply_key, 0, &ascrypto); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + ret = krb5_create_checksum(context, ascrypto, 6, 0, + req_buffer->data, req_buffer->length, + &kp.asChecksum); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + + ret = krb5_crypto_destroy(context, ascrypto); + if (ret) { + krb5_clear_error_message(context); + goto out; + } + ASN1_MALLOC_ENCODE(ReplyKeyPack, buf.data, buf.length, &kp, &size,ret); + free_ReplyKeyPack(&kp); + } + if (ret) { + krb5_set_error_message(context, ret, "ASN.1 encoding of ReplyKeyPack " + "failed (%d)", ret); + goto out; + } + if (buf.length != size) + krb5_abortx(context, "Internal ASN.1 encoder error"); + + { + hx509_query *q; + hx509_cert cert; + + ret = hx509_query_alloc(context->hx509ctx, &q); + if (ret) + goto out; + + hx509_query_match_option(q, HX509_QUERY_OPTION_PRIVATE_KEY); + if (config->pkinit_kdc_friendly_name) + hx509_query_match_friendly_name(q, config->pkinit_kdc_friendly_name); + + ret = hx509_certs_find(context->hx509ctx, + kdc_identity->certs, + q, + &cert); + hx509_query_free(context->hx509ctx, q); + if (ret) + goto out; + + ret = hx509_cms_create_signed_1(context->hx509ctx, + 0, + sdAlg, + buf.data, + buf.length, + NULL, + cert, + cp->peer, + cp->client_anchors, + kdc_identity->certpool, + &signed_data); + *kdc_cert = cert; + } + + krb5_data_free(&buf); + if (ret) + goto out; + + if (cp->type == PKINIT_WIN2K) { + ret = hx509_cms_wrap_ContentInfo(&asn1_oid_id_pkcs7_signedData, + &signed_data, + &buf); + if (ret) + goto out; + krb5_data_free(&signed_data); + signed_data = buf; + } + + ret = hx509_cms_envelope_1(context->hx509ctx, + HX509_CMS_EV_NO_KU_CHECK, + cp->cert, + signed_data.data, signed_data.length, + envelopedAlg, + evAlg, &buf); + if (ret) + goto out; + + ret = _krb5_pk_mk_ContentInfo(context, + &buf, + &asn1_oid_id_pkcs7_envelopedData, + content_info); +out: + if (ret && *kdc_cert) { + hx509_cert_free(*kdc_cert); + *kdc_cert = NULL; + } + + krb5_data_free(&buf); + krb5_data_free(&signed_data); + return ret; +} + +/* + * + */ + +static krb5_error_code +pk_mk_pa_reply_dh(krb5_context context, + krb5_kdc_configuration *config, + pk_client_params *cp, + ContentInfo *content_info, + hx509_cert *kdc_cert) +{ + KDCDHKeyInfo dh_info; + krb5_data signed_data, buf; + ContentInfo contentinfo; + krb5_error_code ret; + hx509_cert cert; + hx509_query *q; + size_t size = 0; + + memset(&contentinfo, 0, sizeof(contentinfo)); + memset(&dh_info, 0, sizeof(dh_info)); + krb5_data_zero(&signed_data); + krb5_data_zero(&buf); + + *kdc_cert = NULL; + + if (cp->keyex == USE_DH) { + DH *kdc_dh = cp->u.dh.key; + heim_integer i; + + ret = BN_to_integer(context, kdc_dh->pub_key, &i); + if (ret) + return ret; + + ASN1_MALLOC_ENCODE(DHPublicKey, buf.data, buf.length, &i, &size, ret); + der_free_heim_integer(&i); + if (ret) { + krb5_set_error_message(context, ret, "ASN.1 encoding of " + "DHPublicKey failed (%d)", ret); + return ret; + } + if (buf.length != size) + krb5_abortx(context, "Internal ASN.1 encoder error"); + + dh_info.subjectPublicKey.length = buf.length * 8; + dh_info.subjectPublicKey.data = buf.data; + krb5_data_zero(&buf); + } else if (cp->keyex == USE_ECDH) { + unsigned char *p; + ret = _kdc_serialize_ecdh_key(context, cp->u.ecdh.key, &p, + &dh_info.subjectPublicKey.length); + if (ret) + goto out; + dh_info.subjectPublicKey.data = p; + } else + krb5_abortx(context, "no keyex selected ?"); + + + dh_info.nonce = cp->nonce; + + ASN1_MALLOC_ENCODE(KDCDHKeyInfo, buf.data, buf.length, &dh_info, &size, + ret); + if (ret) { + krb5_set_error_message(context, ret, "ASN.1 encoding of " + "KdcDHKeyInfo failed (%d)", ret); + goto out; + } + if (buf.length != size) + krb5_abortx(context, "Internal ASN.1 encoder error"); + + /* + * Create the SignedData structure and sign the KdcDHKeyInfo + * filled in above + */ + + ret = hx509_query_alloc(context->hx509ctx, &q); + if (ret) + goto out; + + hx509_query_match_option(q, HX509_QUERY_OPTION_PRIVATE_KEY); + if (config->pkinit_kdc_friendly_name) + hx509_query_match_friendly_name(q, config->pkinit_kdc_friendly_name); + + ret = hx509_certs_find(context->hx509ctx, + kdc_identity->certs, + q, + &cert); + hx509_query_free(context->hx509ctx, q); + if (ret) + goto out; + + ret = hx509_cms_create_signed_1(context->hx509ctx, + 0, + &asn1_oid_id_pkdhkeydata, + buf.data, + buf.length, + NULL, + cert, + cp->peer, + cp->client_anchors, + kdc_identity->certpool, + &signed_data); + if (ret) { + kdc_log(context, config, 0, "Failed signing the DH* reply: %d", ret); + goto out; + } + *kdc_cert = cert; + + ret = _krb5_pk_mk_ContentInfo(context, + &signed_data, + &asn1_oid_id_pkcs7_signedData, + content_info); + if (ret) + goto out; + + out: + if (ret && *kdc_cert) { + hx509_cert_free(*kdc_cert); + *kdc_cert = NULL; + } + + krb5_data_free(&buf); + krb5_data_free(&signed_data); + free_KDCDHKeyInfo(&dh_info); + + return ret; +} + +/* + * + */ + +krb5_error_code +_kdc_pk_mk_pa_reply(astgs_request_t r, pk_client_params *cp) +{ + krb5_kdc_configuration *config = r->config; + krb5_enctype sessionetype = r->sessionetype; + const KDC_REQ *req = &r->req; + const krb5_data *req_buffer = &r->request; + krb5_keyblock *reply_key = &r->reply_key; + krb5_keyblock *sessionkey = &r->session_key; + METHOD_DATA *md = r->rep.padata; + krb5_error_code ret; + void *buf = NULL; + size_t len = 0, size = 0; + krb5_enctype enctype; + int pa_type; + hx509_cert kdc_cert = NULL; + size_t i; + + if (!config->enable_pkinit) { + krb5_clear_error_message(r->context); + return 0; + } + + if (req->req_body.etype.len > 0) { + for (i = 0; i < req->req_body.etype.len; i++) + if (krb5_enctype_valid(r->context, req->req_body.etype.val[i]) == 0) + break; + if (req->req_body.etype.len <= i) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(r->context, ret, + "No valid enctype available from client"); + goto out; + } + enctype = req->req_body.etype.val[i]; + } else + enctype = ETYPE_DES3_CBC_SHA1; + + if (cp->type == PKINIT_27) { + PA_PK_AS_REP rep; + const char *type, *other = ""; + + memset(&rep, 0, sizeof(rep)); + + pa_type = KRB5_PADATA_PK_AS_REP; + + if (cp->keyex == USE_RSA) { + ContentInfo info; + + type = "enckey"; + + rep.element = choice_PA_PK_AS_REP_encKeyPack; + + ret = krb5_generate_random_keyblock(r->context, enctype, + &cp->reply_key); + if (ret) { + free_PA_PK_AS_REP(&rep); + goto out; + } + ret = pk_mk_pa_reply_enckey(r->context, + config, + cp, + req, + req_buffer, + &cp->reply_key, + &info, + &kdc_cert); + if (ret) { + free_PA_PK_AS_REP(&rep); + goto out; + } + ASN1_MALLOC_ENCODE(ContentInfo, rep.u.encKeyPack.data, + rep.u.encKeyPack.length, &info, &size, + ret); + free_ContentInfo(&info); + if (ret) { + krb5_set_error_message(r->context, ret, "encoding of Key ContentInfo " + "failed %d", ret); + free_PA_PK_AS_REP(&rep); + goto out; + } + if (rep.u.encKeyPack.length != size) + krb5_abortx(r->context, "Internal ASN.1 encoder error"); + + ret = krb5_generate_random_keyblock(r->context, sessionetype, + sessionkey); + if (ret) { + free_PA_PK_AS_REP(&rep); + goto out; + } + + } else { + ContentInfo info; + + switch (cp->keyex) { + case USE_DH: type = "dh"; break; + case USE_ECDH: type = "ecdh"; break; + default: krb5_abortx(r->context, "unknown keyex"); break; + } + + if (cp->dh_group_name) + other = cp->dh_group_name; + + rep.element = choice_PA_PK_AS_REP_dhInfo; + + ret = generate_dh_keyblock(r->context, cp, enctype); + if (ret) + return ret; + + ret = pk_mk_pa_reply_dh(r->context, config, + cp, + &info, + &kdc_cert); + if (ret) { + free_PA_PK_AS_REP(&rep); + krb5_set_error_message(r->context, ret, + "create pa-reply-dh " + "failed %d", ret); + goto out; + } + + ASN1_MALLOC_ENCODE(ContentInfo, rep.u.dhInfo.dhSignedData.data, + rep.u.dhInfo.dhSignedData.length, &info, &size, + ret); + free_ContentInfo(&info); + if (ret) { + krb5_set_error_message(r->context, ret, + "encoding of Key ContentInfo " + "failed %d", ret); + free_PA_PK_AS_REP(&rep); + goto out; + } + if (rep.u.encKeyPack.length != size) + krb5_abortx(r->context, "Internal ASN.1 encoder error"); + + /* generate the session key using the method from RFC6112 */ + { + krb5_keyblock kdc_contribution_key; + krb5_crypto reply_crypto; + krb5_crypto kdccont_crypto; + krb5_data p1 = { strlen("PKINIT"), "PKINIT"}; + krb5_data p2 = { strlen("KEYEXCHANGE"), "KEYEXCHANGE"}; + void *kckdata; + size_t kcklen; + EncryptedData kx; + void *kxdata; + size_t kxlen; + + ret = krb5_generate_random_keyblock(r->context, sessionetype, + &kdc_contribution_key); + if (ret) { + free_PA_PK_AS_REP(&rep); + goto out; + } + ret = krb5_crypto_init(r->context, &cp->reply_key, enctype, &reply_crypto); + if (ret) { + krb5_free_keyblock_contents(r->context, &kdc_contribution_key); + free_PA_PK_AS_REP(&rep); + goto out; + } + ret = krb5_crypto_init(r->context, &kdc_contribution_key, sessionetype, &kdccont_crypto); + if (ret) { + krb5_crypto_destroy(r->context, reply_crypto); + krb5_free_keyblock_contents(r->context, &kdc_contribution_key); + free_PA_PK_AS_REP(&rep); + goto out; + } + /* KRB-FX-CF2 */ + ret = krb5_crypto_fx_cf2(r->context, kdccont_crypto, reply_crypto, + &p1, &p2, sessionetype, sessionkey); + krb5_crypto_destroy(r->context, kdccont_crypto); + if (ret) { + krb5_crypto_destroy(r->context, reply_crypto); + krb5_free_keyblock_contents(r->context, &kdc_contribution_key); + free_PA_PK_AS_REP(&rep); + goto out; + } + ASN1_MALLOC_ENCODE(EncryptionKey, kckdata, kcklen, + &kdc_contribution_key, &size, ret); + krb5_free_keyblock_contents(r->context, &kdc_contribution_key); + if (ret) { + krb5_set_error_message(r->context, ret, "encoding of PKINIT-KX Key failed %d", ret); + krb5_crypto_destroy(r->context, reply_crypto); + free_PA_PK_AS_REP(&rep); + goto out; + } + if (kcklen != size) + krb5_abortx(r->context, "Internal ASN.1 encoder error"); + ret = krb5_encrypt_EncryptedData(r->context, reply_crypto, KRB5_KU_PA_PKINIT_KX, + kckdata, kcklen, 0, &kx); + krb5_crypto_destroy(r->context, reply_crypto); + free(kckdata); + if (ret) { + free_PA_PK_AS_REP(&rep); + goto out; + } + ASN1_MALLOC_ENCODE(EncryptedData, kxdata, kxlen, + &kx, &size, ret); + free_EncryptedData(&kx); + if (ret) { + krb5_set_error_message(r->context, ret, + "encoding of PKINIT-KX failed %d", ret); + free_PA_PK_AS_REP(&rep); + goto out; + } + if (kxlen != size) + krb5_abortx(r->context, "Internal ASN.1 encoder error"); + /* Add PA-PKINIT-KX */ + ret = krb5_padata_add(r->context, md, KRB5_PADATA_PKINIT_KX, kxdata, kxlen); + if (ret) { + krb5_set_error_message(r->context, ret, + "Failed adding PKINIT-KX %d", ret); + free(buf); + goto out; + } + } + } + +#define use_btmm_with_enckey 0 + if (use_btmm_with_enckey && rep.element == choice_PA_PK_AS_REP_encKeyPack) { + PA_PK_AS_REP_BTMM btmm; + heim_any any; + + any.data = rep.u.encKeyPack.data; + any.length = rep.u.encKeyPack.length; + + btmm.dhSignedData = NULL; + btmm.encKeyPack = &any; + + ASN1_MALLOC_ENCODE(PA_PK_AS_REP_BTMM, buf, len, &btmm, &size, ret); + } else { + ASN1_MALLOC_ENCODE(PA_PK_AS_REP, buf, len, &rep, &size, ret); + } + + free_PA_PK_AS_REP(&rep); + if (ret) { + krb5_set_error_message(r->context, ret, + "encode PA-PK-AS-REP failed %d", ret); + goto out; + } + if (len != size) + krb5_abortx(r->context, "Internal ASN.1 encoder error"); + + kdc_log(r->context, config, 0, "PKINIT using %s %s", type, other); + + } else if (cp->type == PKINIT_WIN2K) { + PA_PK_AS_REP_Win2k rep; + ContentInfo info; + + if (cp->keyex != USE_RSA) { + ret = KRB5KRB_ERR_GENERIC; + krb5_set_error_message(r->context, ret, + "Win2k PKINIT doesn't support DH"); + goto out; + } + + memset(&rep, 0, sizeof(rep)); + + pa_type = KRB5_PADATA_PK_AS_REP_19; + rep.element = choice_PA_PK_AS_REP_Win2k_encKeyPack; + + ret = krb5_generate_random_keyblock(r->context, enctype, + &cp->reply_key); + if (ret) { + free_PA_PK_AS_REP_Win2k(&rep); + goto out; + } + ret = pk_mk_pa_reply_enckey(r->context, + config, + cp, + req, + req_buffer, + &cp->reply_key, + &info, + &kdc_cert); + if (ret) { + free_PA_PK_AS_REP_Win2k(&rep); + goto out; + } + ASN1_MALLOC_ENCODE(ContentInfo, rep.u.encKeyPack.data, + rep.u.encKeyPack.length, &info, &size, + ret); + free_ContentInfo(&info); + if (ret) { + krb5_set_error_message(r->context, ret, "encoding of Key ContentInfo " + "failed %d", ret); + free_PA_PK_AS_REP_Win2k(&rep); + goto out; + } + if (rep.u.encKeyPack.length != size) + krb5_abortx(r->context, "Internal ASN.1 encoder error"); + + ASN1_MALLOC_ENCODE(PA_PK_AS_REP_Win2k, buf, len, &rep, &size, ret); + free_PA_PK_AS_REP_Win2k(&rep); + if (ret) { + krb5_set_error_message(r->context, ret, + "encode PA-PK-AS-REP-Win2k failed %d", ret); + goto out; + } + if (len != size) + krb5_abortx(r->context, "Internal ASN.1 encoder error"); + + ret = krb5_generate_random_keyblock(r->context, sessionetype, + sessionkey); + if (ret) { + free(buf); + goto out; + } + + } else + krb5_abortx(r->context, "PKINIT internal error"); + + + ret = krb5_padata_add(r->context, md, pa_type, buf, len); + if (ret) { + krb5_set_error_message(r->context, ret, + "Failed adding PA-PK-AS-REP %d", ret); + free(buf); + goto out; + } + + if (config->pkinit_kdc_ocsp_file) { + + if (ocsp.expire == 0 && ocsp.next_update > kdc_time) { + struct stat sb; + int fd; + + krb5_data_free(&ocsp.data); + + ocsp.expire = 0; + ocsp.next_update = kdc_time + 60 * 5; + + fd = open(config->pkinit_kdc_ocsp_file, O_RDONLY); + if (fd < 0) { + kdc_log(r->context, config, 0, + "PKINIT failed to open ocsp data file %d", errno); + goto out_ocsp; + } + ret = fstat(fd, &sb); + if (ret) { + ret = errno; + close(fd); + kdc_log(r->context, config, 0, + "PKINIT failed to stat ocsp data %d", ret); + goto out_ocsp; + } + + ret = krb5_data_alloc(&ocsp.data, sb.st_size); + if (ret) { + close(fd); + kdc_log(r->context, config, 0, + "PKINIT failed to allocate ocsp data %d", ret); + goto out_ocsp; + } + ocsp.data.length = sb.st_size; + ret = read(fd, ocsp.data.data, sb.st_size); + close(fd); + if (ret != sb.st_size) { + kdc_log(r->context, config, 0, + "PKINIT failed to read ocsp data %d", errno); + goto out_ocsp; + } + + ret = hx509_ocsp_verify(r->context->hx509ctx, + kdc_time, + kdc_cert, + 0, + ocsp.data.data, ocsp.data.length, + &ocsp.expire); + if (ret) { + kdc_log(r->context, config, 0, + "PKINIT failed to verify ocsp data %d", ret); + krb5_data_free(&ocsp.data); + ocsp.expire = 0; + } else if (ocsp.expire > 180) { + ocsp.expire -= 180; /* refetch the ocsp before it expires */ + ocsp.next_update = ocsp.expire; + } else { + ocsp.next_update = kdc_time; + } + out_ocsp: + ret = 0; + } + + if (ocsp.expire != 0 && ocsp.expire > kdc_time) { + + ret = krb5_padata_add(r->context, md, + KRB5_PADATA_PA_PK_OCSP_RESPONSE, + ocsp.data.data, ocsp.data.length); + if (ret) { + krb5_set_error_message(r->context, ret, + "Failed adding OCSP response %d", ret); + goto out; + } + } + } + +out: + if (kdc_cert) + hx509_cert_free(kdc_cert); + + if (ret == 0) + ret = krb5_copy_keyblock_contents(r->context, &cp->reply_key, reply_key); + return ret; +} + +static int +match_rfc_san(krb5_context context, + krb5_kdc_configuration *config, + hx509_context hx509ctx, + hx509_cert client_cert, + krb5_const_principal match) +{ + hx509_octet_string_list list; + int ret, found = 0; + size_t i; + + memset(&list, 0 , sizeof(list)); + + ret = hx509_cert_find_subjectAltName_otherName(hx509ctx, + client_cert, + &asn1_oid_id_pkinit_san, + &list); + if (ret) + goto out; + + for (i = 0; !found && i < list.len; i++) { + krb5_principal_data principal; + KRB5PrincipalName kn; + size_t size; + + ret = decode_KRB5PrincipalName(list.val[i].data, + list.val[i].length, + &kn, &size); + if (ret) { + const char *msg = krb5_get_error_message(context, ret); + kdc_log(context, config, 0, + "Decoding Kerberos principal name in certificate failed: %s", msg); + krb5_free_error_message(context, msg); + break; + } + if (size != list.val[i].length) { + kdc_log(context, config, 0, + "Decoded Kerberos principal name did not have expected length"); + return KRB5_KDC_ERR_CLIENT_NAME_MISMATCH; + } + + memset(&principal, 0, sizeof (principal)); + principal.name = kn.principalName; + principal.realm = kn.realm; + + if (krb5_principal_compare(context, &principal, match) == TRUE) + found = 1; + free_KRB5PrincipalName(&kn); + } + +out: + hx509_free_octet_string_list(&list); + if (ret) + return ret; + + if (!found) + return KRB5_KDC_ERR_CLIENT_NAME_MISMATCH; + + return 0; +} + +static int +match_ms_upn_san(krb5_context context, + krb5_kdc_configuration *config, + hx509_context hx509ctx, + hx509_cert client_cert, + HDB *clientdb, + hdb_entry *client) +{ + hx509_octet_string_list list; + krb5_principal principal = NULL; + int ret; + MS_UPN_SAN upn; + size_t size; + + memset(&list, 0 , sizeof(list)); + + ret = hx509_cert_find_subjectAltName_otherName(hx509ctx, + client_cert, + &asn1_oid_id_pkinit_ms_san, + &list); + if (ret) + goto out; + + if (list.len != 1) { + if (list.len) + kdc_log(context, config, 0, + "More than one PKINIT MS UPN SAN"); + else + kdc_log(context, config, 0, + "No PKINIT MS UPN SAN"); + ret = KRB5_KDC_ERR_CLIENT_NAME_MISMATCH; + goto out; + } + + ret = decode_MS_UPN_SAN(list.val[0].data, list.val[0].length, &upn, &size); + if (ret) { + kdc_log(context, config, 0, "Decode of MS-UPN-SAN failed"); + goto out; + } + if (size != list.val[0].length) { + free_MS_UPN_SAN(&upn); + kdc_log(context, config, 0, "Trailing data in MS UPN SAN"); + ret = KRB5_KDC_ERR_CLIENT_NAME_MISMATCH; + goto out; + } + + kdc_log(context, config, 0, "found MS UPN SAN: %s", upn); + + ret = krb5_parse_name(context, upn, &principal); + free_MS_UPN_SAN(&upn); + if (ret) { + kdc_log(context, config, 0, "Failed to parse principal in MS UPN SAN"); + goto out; + } + + if (clientdb->hdb_check_pkinit_ms_upn_match) { + ret = clientdb->hdb_check_pkinit_ms_upn_match(context, clientdb, client, principal); + } else { + + /* + * This is very wrong, but will do for a fallback + */ + strupr(principal->realm); + + if (krb5_principal_compare(context, principal, client->principal) == FALSE) + ret = KRB5_KDC_ERR_CLIENT_NAME_MISMATCH; + } + +out: + if (principal) + krb5_free_principal(context, principal); + hx509_free_octet_string_list(&list); + + return ret; +} + +krb5_error_code +_kdc_pk_check_client(astgs_request_t r, + pk_client_params *cp, + char **subject_name) +{ + krb5_kdc_configuration *config = r->config; + HDB *clientdb = r->clientdb; + hdb_entry *client = r->client; + const HDB_Ext_PKINIT_acl *acl; + const HDB_Ext_PKINIT_cert *pc; + krb5_error_code ret; + hx509_name name; + size_t i; + + if (cp->cert == NULL) { + if (!_kdc_is_anonymous(r->context, client->principal) + && !config->historical_anon_realm) + return KRB5KDC_ERR_BADOPTION; + + *subject_name = strdup("<unauthenticated anonymous client>"); + if (*subject_name == NULL) + return ENOMEM; + return 0; + } + + cp->endtime = hx509_cert_get_notAfter(cp->cert); + cp->max_life = 0; + if (config->pkinit_max_life_from_cert_extension) + cp->max_life = + hx509_cert_get_pkinit_max_life(r->context->hx509ctx, cp->cert, + config->pkinit_max_life_bound); + if (cp->max_life == 0 && config->pkinit_max_life_from_cert > 0) { + cp->max_life = cp->endtime - hx509_cert_get_notBefore(cp->cert); + if (cp->max_life > config->pkinit_max_life_from_cert) + cp->max_life = config->pkinit_max_life_from_cert; + } + + ret = hx509_cert_get_base_subject(r->context->hx509ctx, + cp->cert, + &name); + if (ret) + return ret; + + ret = hx509_name_to_string(name, subject_name); + hx509_name_free(&name); + if (ret) + return ret; + + kdc_log(r->context, config, 0, + "Trying to authorize PKINIT subject DN %s", + *subject_name); + + ret = hdb_entry_get_pkinit_cert(client, &pc); + if (ret == 0 && pc) { + hx509_cert cert; + size_t j; + + for (j = 0; j < pc->len; j++) { + cert = hx509_cert_init_data(r->context->hx509ctx, + pc->val[j].cert.data, + pc->val[j].cert.length, + NULL); + if (cert == NULL) + continue; + ret = hx509_cert_cmp(cert, cp->cert); + hx509_cert_free(cert); + if (ret == 0) { + kdc_log(r->context, config, 5, + "Found matching PKINIT cert in hdb"); + return 0; + } + } + } + + + if (config->pkinit_princ_in_cert) { + ret = match_rfc_san(r->context, config, + r->context->hx509ctx, + cp->cert, + client->principal); + if (ret == 0) { + kdc_log(r->context, config, 5, + "Found matching PKINIT SAN in certificate"); + return 0; + } + ret = match_ms_upn_san(r->context, config, + r->context->hx509ctx, + cp->cert, + clientdb, + client); + if (ret == 0) { + kdc_log(r->context, config, 5, + "Found matching MS UPN SAN in certificate"); + return 0; + } + } + + ret = hdb_entry_get_pkinit_acl(client, &acl); + if (ret == 0 && acl != NULL) { + /* + * Cheat here and compare the generated name with the string + * and not the reverse. + */ + for (i = 0; i < acl->len; i++) { + if (strcmp(*subject_name, acl->val[0].subject) != 0) + continue; + + /* Don't support issuer and anchor checking right now */ + if (acl->val[0].issuer) + continue; + if (acl->val[0].anchor) + continue; + + kdc_log(r->context, config, 5, + "Found matching PKINIT database ACL"); + return 0; + } + } + + for (i = 0; i < principal_mappings.len; i++) { + krb5_boolean b; + + b = krb5_principal_compare(r->context, + client->principal, + principal_mappings.val[i].principal); + if (b == FALSE) + continue; + if (strcmp(principal_mappings.val[i].subject, *subject_name) != 0) + continue; + kdc_log(r->context, config, 5, + "Found matching PKINIT FILE ACL"); + return 0; + } + + ret = KRB5_KDC_ERR_CLIENT_NAME_MISMATCH; + krb5_set_error_message(r->context, ret, + "PKINIT no matching principals for %s", + *subject_name); + + kdc_log(r->context, config, 5, + "PKINIT no matching principals for %s", + *subject_name); + + free(*subject_name); + *subject_name = NULL; + + return ret; +} + +krb5_error_code +_kdc_pk_validate_freshness_token(astgs_request_t r, + pk_client_params *cp) +{ + krb5_error_code ret = 0; + uint8_t *token_data = NULL; + size_t token_len; + uint8_t *remaining_token_data = NULL; + size_t remaining_len; + EncryptedData enc_data; + size_t size; + const hdb_entry *krbtgt = NULL; + krb5_kvno kvno; + const Keys *keys = NULL; + Key *key = NULL; + krb5_crypto crypto; + krb5_data ts_data; + PA_ENC_TS_ENC ts_enc; + long time_diff; + + if (cp->freshness_token == NULL) { + if (r->config->require_pkinit_freshness) { + ret = KRB5KDC_ERR_PREAUTH_FAILED; + kdc_log(r->context, r->config, 0, "PKINIT request is missing required freshness token"); + } + + return ret; + } + + token_data = cp->freshness_token->data; + token_len = cp->freshness_token->length; + + /* Ensure that the token be not empty. */ + if (token_data == NULL) { + kdc_log(r->context, r->config, 0, "Got empty freshness token"); + return KRB5KDC_ERR_PREAUTH_FAILED; + } + + /* Ensure that the two leading bytes are zero. */ + if (token_len < 2 || token_data[0] || token_data[1]) { + kdc_log(r->context, r->config, 0, "Freshness token contains invalid data"); + return KRB5KRB_AP_ERR_MODIFIED; + } + + /* Decrypt the freshness token. */ + + remaining_token_data = token_data + 2; + remaining_len = token_len - 2; + + ret = decode_EncryptedData(remaining_token_data, remaining_len, &enc_data, &size); + if (ret) { + kdc_log(r->context, r->config, 0, "Failed to decode freshness token"); + return KRB5KRB_AP_ERR_MODIFIED; + } + if (size != remaining_len) { + kdc_log(r->context, r->config, 0, "Trailing data in EncryptedData of freshness token"); + free_EncryptedData(&enc_data); + return KRB5KRB_AP_ERR_MODIFIED; + } + + krbtgt = (r->krbtgt != NULL) ? r->krbtgt : r->server; + kvno = (enc_data.kvno != NULL) ? *enc_data.kvno : 0; + + /* We will only accept freshness tokens signed by our local krbtgt. */ + keys = hdb_kvno2keys(r->context, krbtgt, kvno); + if (keys == NULL) { + kdc_log(r->context, r->config, 0, + "No key with kvno %"PRId32" to decrypt freshness token", + kvno); + free_EncryptedData(&enc_data); + return KRB5KDC_ERR_PREAUTH_FAILED; + } + + ret = hdb_enctype2key(r->context, r->client, keys, + enc_data.etype, &key); + if (ret) { + kdc_log(r->context, r->config, 0, + "No key with kvno %"PRId32", enctype %d to decrypt freshness token", + kvno, enc_data.etype); + free_EncryptedData(&enc_data); + return KRB5KDC_ERR_PREAUTH_FAILED; + } + + ret = krb5_crypto_init(r->context, &key->key, 0, &crypto); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + kdc_log(r->context, r->config, 0, + "While attempting to decrypt freshness token, krb5_crypto_init failed: %s", msg); + krb5_free_error_message(r->context, msg); + + free_EncryptedData(&enc_data); + return ret; + } + + ret = krb5_decrypt_EncryptedData(r->context, + crypto, + KRB5_KU_AS_FRESHNESS, + &enc_data, + &ts_data); + krb5_crypto_destroy(r->context, crypto); + free_EncryptedData(&enc_data); + if (ret) { + kdc_log(r->context, r->config, 0, "Failed to decrypt freshness token"); + + free_EncryptedData(&enc_data); + return KRB5KRB_AP_ERR_MODIFIED; + } + + /* Decode the timestamp. */ + + ret = decode_PA_ENC_TS_ENC(ts_data.data, + ts_data.length, + &ts_enc, + &size); + if (ret) { + kdc_log(r->context, r->config, 0, "Failed to decode PA-ENC-TS-ENC in freshness token"); + krb5_data_free(&ts_data); + return KRB5KRB_AP_ERR_MODIFIED; + } + if (size != ts_data.length) { + kdc_log(r->context, r->config, 0, "Trailing data in PA-ENC-TS-ENC of freshness token"); + free_PA_ENC_TS_ENC(&ts_enc); + krb5_data_free(&ts_data); + return KRB5KRB_AP_ERR_MODIFIED; + } + krb5_data_free(&ts_data); + + time_diff = labs(kdc_time - ts_enc.patimestamp); + if (time_diff > r->context->max_skew) { + char token_time[100]; + + krb5_format_time(r->context, ts_enc.patimestamp, + token_time, sizeof(token_time), TRUE); + + kdc_log(r->context, r->config, 4, "Freshness token has too large time skew: " + "time in token %s is out by %ld > %jd seconds — %s", + token_time, + time_diff, + (intmax_t)(r->context->max_skew), + r->cname); + + r->e_text = NULL; + free_PA_ENC_TS_ENC(&ts_enc); + return KRB5_KDC_ERR_PREAUTH_EXPIRED; + } + + free_PA_ENC_TS_ENC(&ts_enc); + return 0; +} + +static krb5_error_code +add_principal_mapping(krb5_context context, + const char *principal_name, + const char * subject) +{ + struct pk_allowed_princ *tmp; + krb5_principal principal; + krb5_error_code ret; + + tmp = realloc(principal_mappings.val, + (principal_mappings.len + 1) * sizeof(*tmp)); + if (tmp == NULL) + return ENOMEM; + principal_mappings.val = tmp; + + ret = krb5_parse_name(context, principal_name, &principal); + if (ret) + return ret; + + principal_mappings.val[principal_mappings.len].principal = principal; + + principal_mappings.val[principal_mappings.len].subject = strdup(subject); + if (principal_mappings.val[principal_mappings.len].subject == NULL) { + krb5_free_principal(context, principal); + return ENOMEM; + } + principal_mappings.len++; + + return 0; +} + +krb5_error_code +_kdc_add_initial_verified_cas(krb5_context context, + krb5_kdc_configuration *config, + pk_client_params *cp, + EncTicketPart *tkt) +{ + AD_INITIAL_VERIFIED_CAS cas; + krb5_error_code ret; + krb5_data data; + size_t size = 0; + + memset(&cas, 0, sizeof(cas)); + + /* XXX add CAs to cas here */ + + ASN1_MALLOC_ENCODE(AD_INITIAL_VERIFIED_CAS, data.data, data.length, + &cas, &size, ret); + if (ret) + return ret; + if (data.length != size) + krb5_abortx(context, "internal asn.1 encoder error"); + + ret = _kdc_tkt_add_if_relevant_ad(context, tkt, + KRB5_AUTHDATA_INITIAL_VERIFIED_CAS, + &data); + krb5_data_free(&data); + return ret; +} + +/* + * + */ + +static void +load_mappings(krb5_context context, const char *fn) +{ + krb5_error_code ret; + char buf[1024]; + unsigned long lineno = 0; + FILE *f; + + f = fopen(fn, "r"); + if (f == NULL) + return; + + while (fgets(buf, sizeof(buf), f) != NULL) { + char *subject_name, *p; + + buf[strcspn(buf, "\n")] = '\0'; + lineno++; + + p = buf + strspn(buf, " \t"); + + if (*p == '#' || *p == '\0') + continue; + + subject_name = strchr(p, ':'); + if (subject_name == NULL) { + krb5_warnx(context, "pkinit mapping file line %lu " + "missing \":\" :%s", + lineno, buf); + continue; + } + *subject_name++ = '\0'; + + ret = add_principal_mapping(context, p, subject_name); + if (ret) { + krb5_warn(context, ret, "failed to add line %lu \":\" :%s\n", + lineno, buf); + continue; + } + } + + fclose(f); +} + +/* + * + */ + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +krb5_kdc_pk_initialize(krb5_context context, + krb5_kdc_configuration *config, + const char *user_id, + const char *anchors, + char **pool, + char **revoke_list) +{ + const char *file; + char *fn = NULL; + krb5_error_code ret; + + file = krb5_config_get_string(context, NULL, + "libdefaults", "moduli", NULL); + + ret = _krb5_parse_moduli(context, file, &moduli); + if (ret) + krb5_err(context, 1, ret, "PKINIT: failed to load moduli file"); + + principal_mappings.len = 0; + principal_mappings.val = NULL; + + ret = _krb5_pk_load_id(context, + &kdc_identity, + user_id, + anchors, + pool, + revoke_list, + NULL, + NULL, + NULL); + if (ret) { + krb5_warn(context, ret, "PKINIT: failed to load ID"); + config->enable_pkinit = 0; + return ret; + } + + { + hx509_query *q; + hx509_cert cert; + + ret = hx509_query_alloc(context->hx509ctx, &q); + if (ret) { + krb5_warnx(context, "PKINIT: out of memory"); + return ENOMEM; + } + + hx509_query_match_option(q, HX509_QUERY_OPTION_PRIVATE_KEY); + if (config->pkinit_kdc_friendly_name) + hx509_query_match_friendly_name(q, config->pkinit_kdc_friendly_name); + + ret = hx509_certs_find(context->hx509ctx, + kdc_identity->certs, + q, + &cert); + hx509_query_free(context->hx509ctx, q); + if (ret == 0) { + if (hx509_cert_check_eku(context->hx509ctx, cert, + &asn1_oid_id_pkkdcekuoid, 0)) { + hx509_name name; + char *str; + ret = hx509_cert_get_subject(cert, &name); + if (ret == 0) { + hx509_name_to_string(name, &str); + krb5_warnx(context, "WARNING Found KDC certificate (%s) " + "is missing the PKINIT KDC EKU, this is bad for " + "interoperability.", str); + hx509_name_free(&name); + free(str); + } + } + hx509_cert_free(cert); + } else + krb5_warnx(context, "PKINIT: failed to find a signing " + "certificate with a public key"); + } + + if (krb5_config_get_bool_default(context, + NULL, + FALSE, + "kdc", + "pkinit_allow_proxy_certificate", + NULL)) + config->pkinit_allow_proxy_certs = 1; + + file = krb5_config_get_string(context, + NULL, + "kdc", + "pkinit_mappings_file", + NULL); + if (file == NULL) { + int aret; + + aret = asprintf(&fn, "%s/pki-mapping", hdb_db_dir(context)); + if (aret == -1) { + krb5_warnx(context, "PKINIT: out of memory"); + return ENOMEM; + } + + file = fn; + } + + load_mappings(context, file); + if (fn) + free(fn); + + return 0; +} + +#endif /* PKINIT */ diff --git a/third_party/heimdal/kdc/process.c b/third_party/heimdal/kdc/process.c new file mode 100644 index 0000000..b53d91f --- /dev/null +++ b/third_party/heimdal/kdc/process.c @@ -0,0 +1,611 @@ +/* + * Copyright (c) 1997-2005 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. + */ + +#include "kdc_locl.h" +#include <vis.h> + +/* + * + */ + +#undef __attribute__ +#define __attribute__(x) + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_vaddreason(kdc_request_t r, const char *fmt, va_list ap) + __attribute__ ((__format__ (__printf__, 2, 0))) +{ + heim_audit_vaddreason((heim_svc_req_desc)r, fmt, ap); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_addreason(kdc_request_t r, const char *fmt, ...) + __attribute__ ((__format__ (__printf__, 2, 3))) +{ + va_list ap; + + va_start(ap, fmt); + heim_audit_vaddreason((heim_svc_req_desc)r, fmt, ap); + va_end(ap); +} + +/* + * append_token adds a token which is optionally a kv-pair and it + * also optionally eats the whitespace. If k == NULL, then it's + * not a kv-pair. + */ + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_vaddkv(kdc_request_t r, int flags, const char *k, + const char *fmt, va_list ap) + __attribute__ ((__format__ (__printf__, 4, 0))) +{ + heim_audit_vaddkv((heim_svc_req_desc)r, flags, k, fmt, ap); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_addkv(kdc_request_t r, int flags, const char *k, + const char *fmt, ...) + __attribute__ ((__format__ (__printf__, 4, 5))) +{ + va_list ap; + + va_start(ap, fmt); + heim_audit_vaddkv((heim_svc_req_desc)r, flags, k, fmt, ap); + va_end(ap); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_addkv_timediff(kdc_request_t r, const char *k, + const struct timeval *start, + const struct timeval *end) +{ + heim_audit_addkv_timediff((heim_svc_req_desc)r,k, start, end); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_setkv_bool(kdc_request_t r, const char *k, krb5_boolean v) +{ + heim_audit_setkv_bool((heim_svc_req_desc)r, k, (int)v); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_addkv_number(kdc_request_t r, const char *k, int64_t v) +{ + heim_audit_addkv_number((heim_svc_req_desc)r, k, v); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_setkv_number(kdc_request_t r, const char *k, int64_t v) +{ + heim_audit_setkv_number((heim_svc_req_desc)r, k, v); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_addkv_object(kdc_request_t r, const char *k, kdc_object_t obj) +{ + heim_audit_addkv_object((heim_svc_req_desc)r, k, obj); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_setkv_object(kdc_request_t r, const char *k, kdc_object_t obj) +{ + heim_audit_setkv_object((heim_svc_req_desc)r, k, obj); +} + +KDC_LIB_FUNCTION kdc_object_t KDC_LIB_CALL +kdc_audit_getkv(kdc_request_t r, const char *k) +{ + return heim_audit_getkv((heim_svc_req_desc)r, k); +} + +/* + * Add up to 3 key value pairs to record HostAddresses from request body or + * PA-TGS ticket or whatever. + */ +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_audit_addaddrs(kdc_request_t r, HostAddresses *a, const char *key) +{ + size_t i; + char buf[128]; + + if (a->len > 3) { + char numkey[32]; + + if (snprintf(numkey, sizeof(numkey), "num%s", key) >= sizeof(numkey)) + numkey[31] = '\0'; + kdc_audit_addkv(r, 0, numkey, "%llu", (unsigned long long)a->len); + } + + for (i = 0; i < 3 && i < a->len; i++) { + if (krb5_print_address(&a->val[i], buf, sizeof(buf), NULL) == 0) + kdc_audit_addkv(r, 0, key, "%s", buf); + } +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +_kdc_audit_trail(kdc_request_t r, krb5_error_code ret) +{ + const char *retname = NULL; + + /* Get a symbolic name for some error codes */ +#define CASE(x) case x : retname = #x; break + switch (ret ? ret : r->error_code) { + 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(KRB5KDC_ERR_BADOPTION); + CASE(KRB5KDC_ERR_CANNOT_POSTDATE); + CASE(KRB5KDC_ERR_CLIENT_NOTYET); + CASE(KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN); + CASE(KRB5KDC_ERR_ETYPE_NOSUPP); + CASE(KRB5KDC_ERR_KEY_EXPIRED); + CASE(KRB5KDC_ERR_NAME_EXP); + CASE(KRB5KDC_ERR_NEVER_VALID); + CASE(KRB5KDC_ERR_NONE); + CASE(KRB5KDC_ERR_NULL_KEY); + CASE(KRB5KDC_ERR_PADATA_TYPE_NOSUPP); + CASE(KRB5KDC_ERR_POLICY); + CASE(KRB5KDC_ERR_PREAUTH_FAILED); + CASE(KRB5KDC_ERR_PREAUTH_REQUIRED); + CASE(KRB5KDC_ERR_SERVER_NOMATCH); + CASE(KRB5KDC_ERR_SERVICE_EXP); + CASE(KRB5KDC_ERR_SERVICE_NOTYET); + CASE(KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN); + CASE(KRB5KDC_ERR_TRTYPE_NOSUPP); + CASE(KRB5KRB_AP_ERR_BADADDR); + CASE(KRB5KRB_AP_ERR_BADDIRECTION); + CASE(KRB5KRB_AP_ERR_BAD_INTEGRITY); + CASE(KRB5KRB_AP_ERR_BADKEYVER); + CASE(KRB5KRB_AP_ERR_BADMATCH); + CASE(KRB5KRB_AP_ERR_BADORDER); + CASE(KRB5KRB_AP_ERR_BADSEQ); + CASE(KRB5KRB_AP_ERR_BADVERSION); + CASE(KRB5KRB_AP_ERR_ILL_CR_TKT); + CASE(KRB5KRB_AP_ERR_INAPP_CKSUM); + CASE(KRB5KRB_AP_ERR_METHOD); + CASE(KRB5KRB_AP_ERR_MODIFIED); + CASE(KRB5KRB_AP_ERR_MSG_TYPE); + CASE(KRB5KRB_AP_ERR_MUT_FAIL); + CASE(KRB5KRB_AP_ERR_NOKEY); + CASE(KRB5KRB_AP_ERR_NOT_US); + CASE(KRB5KRB_AP_ERR_REPEAT); + CASE(KRB5KRB_AP_ERR_SKEW); + CASE(KRB5KRB_AP_ERR_TKT_EXPIRED); + CASE(KRB5KRB_AP_ERR_TKT_INVALID); + CASE(KRB5KRB_AP_ERR_TKT_NYV); + CASE(KRB5KRB_AP_ERR_V4_REPLY); + CASE(KRB5KRB_AP_PATH_NOT_ACCEPTED); + CASE(KRB5KRB_AP_WRONG_PRINC); + CASE(KRB5KRB_ERR_FIELD_TOOLONG); + CASE(KRB5KRB_ERR_GENERIC); + CASE(KRB5KRB_ERR_RESPONSE_TOO_BIG); + + case 0: + retname = "SUCCESS"; + break; + default: + retname = NULL; + break; + } + + /* Let's save a few bytes */ +#define PREFIX "KRB5KDC_" + if (retname && strncmp(PREFIX, retname, strlen(PREFIX)) == 0) + retname += strlen(PREFIX); +#undef PREFIX + + heim_audit_trail((heim_svc_req_desc)r, ret, retname); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +krb5_kdc_update_time(struct timeval *tv) +{ + if (tv == NULL) + gettimeofday(&_kdc_now, NULL); + else + _kdc_now = *tv; +} + +KDC_LIB_FUNCTION struct timeval KDC_LIB_CALL +krb5_kdc_get_time(void) +{ + return _kdc_now; +} + + +#define EXTEND_REQUEST_T(LHS, RHS) do { \ + RHS = realloc(LHS, sizeof(*RHS)); \ + if (!RHS) \ + return krb5_enomem((LHS)->context); \ + LHS = (void *)RHS; \ + memset(((char *)LHS) + sizeof(*LHS), \ + 0x0, \ + sizeof(*RHS) - sizeof(*LHS)); \ + } while (0) + +static krb5_error_code +kdc_as_req(kdc_request_t *rptr, int *claim) +{ + astgs_request_t r; + krb5_error_code ret; + size_t len; + + /* We must free things in the extensions */ + EXTEND_REQUEST_T(*rptr, r); + + ret = decode_AS_REQ(r->request.data, r->request.length, &r->req, &len); + if (ret) + return ret; + + r->reqtype = "AS-REQ"; + r->use_request_t = 1; + *claim = 1; + + ret = _kdc_as_rep(r); + free_AS_REQ(&r->req); + return ret; +} + + +static krb5_error_code +kdc_tgs_req(kdc_request_t *rptr, int *claim) +{ + astgs_request_t r; + krb5_error_code ret; + size_t len; + + /* We must free things in the extensions */ + EXTEND_REQUEST_T(*rptr, r); + + ret = decode_TGS_REQ(r->request.data, r->request.length, &r->req, &len); + if (ret) + return ret; + + r->reqtype = "TGS-REQ"; + r->use_request_t = 1; + *claim = 1; + + ret = _kdc_tgs_rep(r); + free_TGS_REQ(&r->req); + return ret; +} + +#ifdef DIGEST + +static krb5_error_code +kdc_digest(kdc_request_t *rptr, int *claim) +{ + kdc_request_t r; + DigestREQ digestreq; + krb5_error_code ret; + size_t len; + + r = *rptr; + + ret = decode_DigestREQ(r->request.data, r->request.length, + &digestreq, &len); + if (ret) + return ret; + + r->use_request_t = 0; + *claim = 1; + + ret = _kdc_do_digest(r->context, r->config, &digestreq, + r->reply, r->from, r->addr); + free_DigestREQ(&digestreq); + return ret; +} + +#endif + +#ifdef KX509 + +static krb5_error_code +kdc_kx509(kdc_request_t *rptr, int *claim) +{ + kx509_req_context r; + krb5_error_code ret; + + /* We must free things in the extensions */ + EXTEND_REQUEST_T(*rptr, r); + + ret = _kdc_try_kx509_request(r); + if (ret) + return ret; + + r->use_request_t = 1; + r->reqtype = "KX509"; + *claim = 1; + + return _kdc_do_kx509(r); /* Must clean up the req struct extensions */ +} + +#endif + + +static struct krb5_kdc_service services[] = { + { KS_KRB5, "AS-REQ", kdc_as_req }, + { KS_KRB5, "TGS-REQ", kdc_tgs_req }, +#ifdef DIGEST + { 0, "DIGEST", kdc_digest }, +#endif +#ifdef KX509 + { 0, "KX509", kdc_kx509 }, +#endif + { 0, NULL, NULL } +}; + +static int +process_request(krb5_context context, + krb5_kdc_configuration *config, + unsigned int krb5_only, + unsigned char *buf, + size_t len, + krb5_data *reply, + krb5_boolean *prependlength, + const char *from, + struct sockaddr *addr, + int datagram_reply) +{ + kdc_request_t r; + krb5_error_code ret; + unsigned int i; + int claim = 0; + + r = calloc(sizeof(*r), 1); + if (!r) + return krb5_enomem(context); + + r->context = context; + r->hcontext = context->hcontext; + r->config = config; + r->logf = config->logf; + r->from = from; + r->addr = addr; + r->request.data = buf; + r->request.length = len; + r->datagram_reply = datagram_reply; + r->reply = reply; + r->kv = heim_dict_create(10); + r->attributes = heim_dict_create(1); + if (r->kv == NULL || r->attributes == NULL) { + heim_release(r->kv); + heim_release(r->attributes); + free(r); + return krb5_enomem(context); + } + + gettimeofday(&r->tv_start, NULL); + + for (i = 0; services[i].process != NULL; i++) { + if (krb5_only && (services[i].flags & KS_KRB5) == 0) + continue; + kdc_log(context, config, 7, "Probing for %s", services[i].name); + ret = (*services[i].process)(&r, &claim); + if (claim) { + if (prependlength && services[i].flags & KS_NO_LENGTH) + *prependlength = 0; + + if (r->use_request_t) { + gettimeofday(&r->tv_end, NULL); + _kdc_audit_trail(r, ret); + free(r->cname); + free(r->sname); + free(r->e_text_buf); + krb5_data_free(&r->e_data); + } + + heim_release(r->reason); + heim_release(r->kv); + heim_release(r->attributes); + free(r); + return ret; + } + } + + heim_release(r->reason); + heim_release(r->kv); + heim_release(r->attributes); + free(r); + return -1; +} + +/* + * handle the request in `buf, len', from `addr' (or `from' as a string), + * sending a reply in `reply'. + */ + +KDC_LIB_FUNCTION int KDC_LIB_CALL +krb5_kdc_process_request(krb5_context context, + krb5_kdc_configuration *config, + unsigned char *buf, + size_t len, + krb5_data *reply, + krb5_boolean *prependlength, + const char *from, + struct sockaddr *addr, + int datagram_reply) +{ + return process_request(context, config, 0, buf, len, reply, prependlength, + from, addr, datagram_reply); +} + +/* + * handle the request in `buf, len', from `addr' (or `from' as a string), + * sending a reply in `reply'. + * + * This only processes krb5 requests + */ + +KDC_LIB_FUNCTION int KDC_LIB_CALL +krb5_kdc_process_krb5_request(krb5_context context, + krb5_kdc_configuration *config, + unsigned char *buf, + size_t len, + krb5_data *reply, + const char *from, + struct sockaddr *addr, + int datagram_reply) +{ + return process_request(context, config, 1, buf, len, reply, NULL, + from, addr, datagram_reply); +} + + +/* + * + */ + +KDC_LIB_FUNCTION int KDC_LIB_CALL +krb5_kdc_save_request(krb5_context context, + const char *fn, + const unsigned char *buf, + size_t len, + const krb5_data *reply, + const struct sockaddr *sa) +{ + krb5_storage *sp; + krb5_address a; + int fd = -1; + int ret = 0; + uint32_t t; + krb5_data d; + + memset(&a, 0, sizeof(a)); + + d.data = rk_UNCONST(buf); /* do not free here */ + d.length = len; + t = _kdc_now.tv_sec; + + sp = krb5_storage_emem(); + if (sp == NULL) + ret = krb5_enomem(context); + + if (ret == 0) + ret = krb5_sockaddr2address(context, sa, &a); + if (ret == 0) + ret = krb5_store_uint32(sp, 1); + if (ret == 0) + ret = krb5_store_uint32(sp, t); + if (ret == 0) + ret = krb5_store_address(sp, a); + if (ret == 0) + ret = krb5_store_data(sp, d); + d.length = 0; + d.data = NULL; + if (ret == 0) { + Der_class cl; + Der_type ty; + unsigned int tag; + ret = der_get_tag (reply->data, reply->length, + &cl, &ty, &tag, NULL); + if (ret) { + ret = krb5_store_uint32(sp, 0xffffffff); + if (ret == 0) + ret = krb5_store_uint32(sp, 0xffffffff); + } else { + ret = krb5_store_uint32(sp, MAKE_TAG(cl, ty, 0)); + if (ret == 0) + ret = krb5_store_uint32(sp, tag); + } + } + + if (ret == 0) + ret = krb5_storage_to_data(sp, &d); + krb5_storage_free(sp); + sp = NULL; + + /* + * We've got KDC concurrency, so we're going to try to do a single O_APPEND + * write(2). Hopefully we manage to write enough of the header that one + * can skip this request if it fails to write completely. + */ + if (ret == 0) + fd = open(fn, O_WRONLY|O_CREAT|O_APPEND, 0600); + if (fd < 0) + krb5_set_error_message(context, ret = errno, "Failed to open: %s", fn); + if (ret == 0) { + sp = krb5_storage_from_fd(fd); + if (sp == NULL) + krb5_set_error_message(context, ret = ENOMEM, + "Storage failed to open fd"); + } + (void) close(fd); + if (ret == 0) + ret = krb5_store_data(sp, d); + krb5_free_address(context, &a); + /* + * krb5_storage_free() currently always returns 0, but for FDs it sets + * errno to whatever close() set it to if it failed. + */ + errno = 0; + if (ret == 0) + ret = krb5_storage_free(sp); + else + (void) krb5_storage_free(sp); + if (ret == 0 && errno) + ret = errno; + + return ret; +} + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +kdc_request_set_attribute(kdc_request_t r, kdc_object_t key, kdc_object_t value) +{ + return heim_dict_set_value(r->attributes, key, value); +} + +KDC_LIB_FUNCTION kdc_object_t KDC_LIB_CALL +kdc_request_get_attribute(kdc_request_t r, kdc_object_t key) +{ + return heim_dict_get_value(r->attributes, key); +} + +KDC_LIB_FUNCTION kdc_object_t KDC_LIB_CALL +kdc_request_copy_attribute(kdc_request_t r, kdc_object_t key) +{ + return heim_dict_copy_value(r->attributes, key); +} + +KDC_LIB_FUNCTION void KDC_LIB_CALL +kdc_request_delete_attribute(kdc_request_t r, kdc_object_t key) +{ + heim_dict_delete_key(r->attributes, key); +} diff --git a/third_party/heimdal/kdc/set_dbinfo.c b/third_party/heimdal/kdc/set_dbinfo.c new file mode 100644 index 0000000..683eaaf --- /dev/null +++ b/third_party/heimdal/kdc/set_dbinfo.c @@ -0,0 +1,113 @@ +/* + * Copyright (c) 1997-2007 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Portions Copyright (c) 2009 Apple Inc. 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. + */ + +#include "kdc_locl.h" + +static krb5_error_code +add_db(krb5_context context, struct krb5_kdc_configuration *c, + const char *conf, const char *master_key) +{ + krb5_error_code ret; + void *ptr; + + ptr = realloc(c->db, (c->num_db + 1) * sizeof(*c->db)); + if (ptr == NULL) { + krb5_set_error_message(context, ENOMEM, "malloc: out of memory"); + return ENOMEM; + } + c->db = ptr; + + ret = hdb_create(context, &c->db[c->num_db], conf); + if(ret) + return ret; + + c->num_db++; + + if (master_key) { + ret = hdb_set_master_keyfile(context, c->db[c->num_db - 1], master_key); + if (ret) + return ret; + } + + return 0; +} + +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +krb5_kdc_set_dbinfo(krb5_context context, struct krb5_kdc_configuration *c) +{ + struct hdb_dbinfo *info, *d; + krb5_error_code ret; + int i; + + /* fetch the databases */ + ret = hdb_get_dbinfo(context, &info); + if (ret) + return ret; + + d = NULL; + while ((d = hdb_dbinfo_get_next(info, d)) != NULL) { + + ret = add_db(context, c, + hdb_dbinfo_get_dbname(context, d), + hdb_dbinfo_get_mkey_file(context, d)); + if (ret) + goto out; + + kdc_log(context, c, 3, "label: %s", + hdb_dbinfo_get_label(context, d)); + kdc_log(context, c, 3, "\tdbname: %s", + hdb_dbinfo_get_dbname(context, d)); + kdc_log(context, c, 3, "\tmkey_file: %s", + hdb_dbinfo_get_mkey_file(context, d)); + kdc_log(context, c, 3, "\tacl_file: %s", + hdb_dbinfo_get_acl_file(context, d)); + } + hdb_free_dbinfo(context, &info); + + return 0; +out: + for (i = 0; i < c->num_db; i++) + if (c->db[i] && c->db[i]->hdb_destroy) + (*c->db[i]->hdb_destroy)(context, c->db[i]); + c->num_db = 0; + free(c->db); + c->db = NULL; + + hdb_free_dbinfo(context, &info); + + return ret; +} + + diff --git a/third_party/heimdal/kdc/string2key-version.rc b/third_party/heimdal/kdc/string2key-version.rc new file mode 100644 index 0000000..120ef4b --- /dev/null +++ b/third_party/heimdal/kdc/string2key-version.rc @@ -0,0 +1,36 @@ +/*********************************************************************** + * Copyright (c) 2010, Secure Endpoints Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - 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. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 + * COPYRIGHT HOLDER 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 RC_FILE_TYPE VFT_APP +#define RC_FILE_DESC_0409 "Password to Key Mapper" +#define RC_FILE_ORIG_0409 "string2key.exe" + +#include "../windows/version.rc" diff --git a/third_party/heimdal/kdc/string2key.8 b/third_party/heimdal/kdc/string2key.8 new file mode 100644 index 0000000..1b38d33 --- /dev/null +++ b/third_party/heimdal/kdc/string2key.8 @@ -0,0 +1,85 @@ +.\" Copyright (c) 2000 - 2002 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. +.\" +.\" $Id$ +.\" +.Dd March 4, 2000 +.Dt STRING2KEY 8 +.Os HEIMDAL +.Sh NAME +.Nm string2key +.Nd map a password into a key +.Sh SYNOPSIS +.Nm +.Op Fl 5 | Fl Fl version5 +.Op Fl 4 | Fl Fl version4 +.Op Fl a | Fl Fl afs +.Oo Fl c Ar cell \*(Ba Xo +.Fl Fl cell= Ns Ar cell +.Xc +.Oc +.Oo Fl w Ar password \*(Ba Xo +.Fl Fl password= Ns Ar password +.Xc +.Oc +.Oo Fl p Ar principal \*(Ba Xo +.Fl Fl principal= Ns Ar principal +.Xc +.Oc +.Oo Fl k Ar string \*(Ba Xo +.Fl Fl keytype= Ns Ar string +.Xc +.Oc +.Ar password +.Sh DESCRIPTION +.Nm +performs the string-to-key function. +This is useful when you want to handle the raw key instead of the password. +Supported options: +.Bl -tag -width Ds +.It Fl 5 , Fl Fl version5 +Output Kerberos v5 string-to-key +.It Fl 4 , Fl Fl version4 +Output Kerberos v4 string-to-key +.It Fl a , Fl Fl afs +Output AFS string-to-key +.It Fl c Ar cell , Fl Fl cell= Ns Ar cell +AFS cell to use +.It Fl w Ar password , Fl Fl password= Ns Ar password +Password to use +.It Fl p Ar principal , Fl Fl principal= Ns Ar principal +Kerberos v5 principal to use +.It Fl k Ar string , Fl Fl keytype= Ns Ar string +Keytype +.It Fl Fl version +print version +.It Fl Fl help +.El diff --git a/third_party/heimdal/kdc/string2key.c b/third_party/heimdal/kdc/string2key.c new file mode 100644 index 0000000..4b9c62e --- /dev/null +++ b/third_party/heimdal/kdc/string2key.c @@ -0,0 +1,189 @@ +/* + * Copyright (c) 1997-2003 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. + */ + +#include "headers.h" +#include <getarg.h> + +int version5; +int version4; +int afs; +char *principal; +char *cell; +char *password; +const char *keytype_str = "des3-cbc-sha1"; +int version; +int help; + +struct getargs args[] = { + { "version5", '5', arg_flag, &version5, "Output Kerberos v5 string-to-key", + NULL }, + { "version4", '4', arg_flag, &version4, "Output Kerberos v4 string-to-key", + NULL }, + { "afs", 'a', arg_flag, &afs, "Output AFS string-to-key", NULL }, + { "cell", 'c', arg_string, &cell, "AFS cell to use", "cell" }, + { "password", 'w', arg_string, &password, "Password to use", "password" }, + { "principal",'p', arg_string, &principal, "Kerberos v5 principal to use", "principal" }, + { "keytype", 'k', arg_string, rk_UNCONST(&keytype_str), "Keytype", NULL }, + { "version", 0, arg_flag, &version, "print version", NULL }, + { "help", 0, arg_flag, &help, NULL, NULL } +}; + +int num_args = sizeof(args) / sizeof(args[0]); + +static void +usage(int status) +{ + arg_printusage (args, num_args, NULL, "password"); + exit(status); +} + +static void +tokey(krb5_context context, + krb5_enctype enctype, + const char *pw, + krb5_salt salt, + const char *label) +{ + krb5_error_code ret; + size_t i; + krb5_keyblock key; + char *e; + + ret = krb5_string_to_key_salt(context, enctype, pw, salt, &key); + if (ret) + krb5_err(context, 1, ret, "krb5_string_to_key_salt"); + ret = krb5_enctype_to_string(context, enctype, &e); + if (ret) + krb5_err(context, 1, ret, "krb5_enctype_to_string"); + printf(label, e); + printf(": "); + for(i = 0; i < key.keyvalue.length; i++) + printf("%02x", ((unsigned char*)key.keyvalue.data)[i]); + printf("\n"); + krb5_free_keyblock_contents(context, &key); + free(e); +} + +int +main(int argc, char **argv) +{ + krb5_context context; + krb5_principal princ; + krb5_salt salt; + int optidx; + char buf[1024]; + krb5_enctype etype; + krb5_error_code ret; + + optidx = krb5_program_setup(&context, argc, argv, args, num_args, NULL); + + if(help) + usage(0); + + if(version){ + print_version (NULL); + return 0; + } + + argc -= optidx; + argv += optidx; + + if (argc > 1) + usage(1); + + if(!version5 && !version4 && !afs) + version5 = 1; + + ret = krb5_string_to_enctype(context, keytype_str, &etype); + if(ret) + krb5_err(context, 1, ret, "krb5_string_to_enctype"); + + if((etype != ETYPE_DES_CBC_CRC && + etype != ETYPE_DES_CBC_MD4 && + etype != ETYPE_DES_CBC_MD5) && + (afs || version4)) { + if(!version5) { + etype = ETYPE_DES_CBC_CRC; + } else { + krb5_errx(context, 1, + "DES is the only valid keytype for AFS and Kerberos 4"); + } + } + + if(version5 && principal == NULL){ + printf("Kerberos v5 principal: "); + if(fgets(buf, sizeof(buf), stdin) == NULL) + return 1; + buf[strcspn(buf, "\r\n")] = '\0'; + principal = estrdup(buf); + } + if(afs && cell == NULL){ + printf("AFS cell: "); + if(fgets(buf, sizeof(buf), stdin) == NULL) + return 1; + buf[strcspn(buf, "\r\n")] = '\0'; + cell = estrdup(buf); + } + if(argv[0]) + password = argv[0]; + if(password == NULL){ + if(UI_UTIL_read_pw_string(buf, sizeof(buf), "Password: ", 0)) + return 1; + password = buf; + } + + if(version5){ + ret = krb5_parse_name(context, principal, &princ); + if (ret) + krb5_err(context, 1, ret, "failed to unparse name: %s", principal); + ret = krb5_get_pw_salt(context, princ, &salt); + if (ret) + krb5_err(context, 1, ret, "failed to get salt for %s", principal); + + tokey(context, etype, password, salt, "Kerberos 5 (%s)"); + krb5_free_salt(context, salt); + } + if(version4){ + salt.salttype = KRB5_PW_SALT; + salt.saltvalue.length = 0; + salt.saltvalue.data = NULL; + tokey(context, ETYPE_DES_CBC_MD5, password, salt, "Kerberos 4"); + } + if(afs){ + salt.salttype = KRB5_AFS3_SALT; + salt.saltvalue.length = strlen(cell); + salt.saltvalue.data = cell; + tokey(context, ETYPE_DES_CBC_MD5, password, salt, "AFS"); + } + return 0; +} diff --git a/third_party/heimdal/kdc/test_csr_authorizer.c b/third_party/heimdal/kdc/test_csr_authorizer.c new file mode 100644 index 0000000..200af16 --- /dev/null +++ b/third_party/heimdal/kdc/test_csr_authorizer.c @@ -0,0 +1,597 @@ +/* + * Copyright (c) 2022 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. + */ +#include "kdc_locl.h" +#include <heim-ipc.h> + +/* + * This program implements two things: + * + * - a utility for testing the `kdc_authorize_csr()' function and the plugins + * that uses, + * + * and + * + * - a server for the IPC authorizer. + * + * For the latter, requested certificate SANs and EKUs are authorized by + * checking for existence of files of the form: + * + * /<path>/<princ>/<ext>-<value> + * + * where <path> is given as an option. + * + * <princ> is a requesting client principal name with all characters other than + * alphanumeric, '-', '_', and non-leading '.' URL-encoded. + * + * <ext> is one of: + * + * - pkinit (SAN) + * - xmpp (SAN) + * - email (SAN) + * - ms-upn (SAN) + * - dnsname (SAN) + * - eku (EKU OID) + * + * and <value> is a display form of the SAN or EKU OID, with SANs URL-encoded + * just like principal names (see above). + * + * OIDs are of the form "1.2.3.4.5". + * + * Only digitalSignature and nonRepudiation key usage values are permitted. + */ + +static int help_flag; +static int version_flag; +static int daemon_flag; +static int daemon_child_flag = -1; +static int ignore_flag = 0; +static int server_flag = 0; +static const char *app_string = "kdc"; +static const char *socket_dir; +static const char *authz_dir; + +struct getargs args[] = { + { "help", 'h', arg_flag, &help_flag, + "Print usage message", NULL }, + { "version", 'v', arg_flag, &version_flag, + "Print version", NULL }, + { "app", 'a', arg_string, &app_string, + "App to test (kdc or bx509); default: kdc", "APPNAME" }, + { "socket-dir", 'S', arg_string, &socket_dir, + "IPC socket directory", "DIR" }, + { "authorization-dir", 'A', arg_string, &authz_dir, + "authorization directory", "DIR" }, + { "server", '\0', arg_flag, &server_flag, + "Server mode", NULL }, + { "ignore", 'I', arg_flag, &ignore_flag, + "ignore requests", NULL }, + { "daemon", 'd', arg_flag, &daemon_flag, + "daemonize", NULL }, + { "daemon-child", '\0', arg_flag, &daemon_child_flag, + "internal-use-only option", NULL }, +}; +size_t num_args = sizeof(args) / sizeof(args[0]); + +static int +usage(int e) +{ + arg_printusage(args, num_args, NULL, "PATH-TO-DER-CSR PRINCIPAL"); + fprintf(stderr, + "\tExercise CSR authorization plugins for a given CSR for a\n" + "\tgiven principal.\n\n" + "\tServer-mode (--server) looks for files in the \n" + "\t--authorization-dir DIR directory named:\n" + "\n" + "\t\teku=OID\n" + "\t\tsan_pkinit=PRINCIPAL\n" + "\t\tsan_ms_upn=PRINCIPAL\n" + "\t\tsan_dnsname=DOMAINNAME\n" + "\t\tsan_xmpp=JABBER-ID\n" + "\t\tsan_email=EMAIL\n" + "\n" + "\tClient-mode positional arguments are:\n\n" + "\t\tPATH-TO-DER-CSR PRETEND-CLIENT-PRINCIPAL [...]\n\n" + "\twhere {...} are requested features that must be granted\n" + "\tif the request is only partially authorized.\n\n" + "\tClient example:\n\t\t%s PKCS10:/tmp/csr.der foo@TEST.H5L.SE\n", + getprogname()); + exit(e); + return e; +} + +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 + + _krb5_load_plugins(context, "kdc", (const char **)dirs); + +#ifndef _WIN32 + krb5_config_free_strings(cfdirs); +#endif +} + +static char *string_encode(const char *); +static int stat_authz(const char *, const char *); + +static krb5_error_code +authorize(const char *subject, const char *thing) +{ + krb5_error_code ret; + char *s = NULL; + + s = string_encode(subject); + if (s == NULL) + return ENOMEM; + + ret = stat_authz(s, thing); + if (ret == ENOENT) + ret = stat_authz(s, "all"); + if (ret == ENOENT) + ret = EACCES; + free(s); + return ret; +} + +static void +service(void *ctx, + const heim_octet_string *req, + const heim_icred cred, + heim_ipc_complete complete_cb, + heim_sipc_call complete_cb_data) +{ + krb5_error_code ret = 0; + struct rk_strpool *result = NULL; + krb5_data rep; + const char *subject; + char *cmd; + char *next = NULL; + char *res = NULL; + char *tok; + char *s; + int none_granted = 1; + int all_granted = 1; + int first = 1; + + /* + * A krb5_context and log facility for logging would be nice, but this is + * all just for testing. + */ + + (void)ctx; + + cmd = strndup(req->data, req->length); + if (cmd == NULL) + errx(1, "Out of memory"); + + if (strncmp(cmd, "check ", sizeof("check ") - 1) != 0) { + rep.data = "Invalid request command (must be \"check ...\")"; + rep.length = sizeof("Invalid request command (must be \"check ...\")") - 1; + (*complete_cb)(complete_cb_data, EINVAL, &rep); + free(cmd); + return; + } + + s = cmd + sizeof("check ") - 1; + subject = strtok_r(s, " ", &next); + s = NULL; + + while ((tok = strtok_r(s, " ", &next))) { + int ret2; + + ret2 = authorize(subject, tok); + result = rk_strpoolprintf(result, "%s%s:%s", + first ? "" : ",", + tok, + ret2 == 0 ? "granted" : "denied"); + if (ret2 == 0) + none_granted = 0; + else + all_granted = 0; + + if (ret2 != 0 && ret == 0) + ret = ret2; + + first = 0; + } + free(cmd); + + if (ret == 0 && all_granted) { + rk_strpoolfree(result); + + rep.data = "granted"; + rep.length = sizeof("granted") - 1; + (*complete_cb)(complete_cb_data, 0, &rep); + return; + } + + if (none_granted && ignore_flag) { + rk_strpoolfree(result); + + rep.data = "ignore"; + rep.length = sizeof("ignore") - 1; + (*complete_cb)(complete_cb_data, KRB5_PLUGIN_NO_HANDLE, &rep); + return; + } + + s = rk_strpoolcollect(result); /* frees `result' */ + if (s == NULL) { + rep.data = "denied out-of-memory"; + rep.length = sizeof("denied out-of-memory") - 1; + (*complete_cb)(complete_cb_data, KRB5_PLUGIN_NO_HANDLE, &rep); + return; + } + + if (asprintf(&res, "denied %s", s) == -1) + errx(1, "Out of memory"); + if (res == NULL) + errx(1, "Out of memory"); + + rep.data = res; + rep.length = strlen(res); + + (*complete_cb)(complete_cb_data, ret, &rep); + free(res); + free(s); +} + +static char * +make_feature_argument(const char *kind, + hx509_san_type san_type, + const char *value) +{ + const char *san_type_str = NULL; + char *s = NULL; + + if (strcmp(kind, "san") != 0) { + if (asprintf(&s, "%s=%s", kind, value) == -1 || s == NULL) + errx(1, "Out of memory"); + return s; + } + + switch (san_type) { + case HX509_SAN_TYPE_EMAIL: + san_type_str = "email"; + break; + case HX509_SAN_TYPE_DNSNAME: + san_type_str = "dnsname"; + break; + case HX509_SAN_TYPE_DN: + san_type_str = "dn"; + break; + case HX509_SAN_TYPE_REGISTERED_ID: + san_type_str = "registered_id"; + break; + case HX509_SAN_TYPE_XMPP: + san_type_str = "xmpp"; + break; + case HX509_SAN_TYPE_PKINIT: + case HX509_SAN_TYPE_MS_UPN: + san_type_str = "pkinit"; + break; + case HX509_SAN_TYPE_DNSSRV: + san_type_str = "dnssrv"; + break; + default: + warnx("SAN type not supported"); + return ""; + } + + if (asprintf(&s, "san_%s=%s", san_type_str, value) == -1 || s == NULL) + errx(1, "Out of memory"); + return s; +} + +int +main(int argc, char **argv) +{ + krb5_log_facility *logf; + krb5_error_code ret; + krb5_context context; + hx509_request csr; + krb5_principal princ = NULL; + const char *argv0 = argv[0]; + int optidx = 0; + + setprogname(argv[0]); + if (getarg(args, num_args, argc, argv, &optidx)) + return usage(1); + if (help_flag) + return usage(0); + if (version_flag) { + print_version(argv[0]); + return 0; + } + + if ((errno = krb5_init_context(&context))) + err(1, "Could not initialize krb5_context"); + if ((ret = krb5_initlog(context, argv0, &logf)) || + (ret = krb5_addlog_dest(context, logf, "0-5/STDERR"))) + krb5_err(context, 1, ret, "Could not set up logging to stderr"); + load_plugins(context); + + if (server_flag && daemon_flag) + daemon_child_flag = roken_detach_prep(argc, argv, "--daemon-child"); + + argc -= optidx; + argv += optidx; + + if (socket_dir) + setenv("HEIM_IPC_DIR", socket_dir, 1); + + if (server_flag) { + const char *svc; + heim_sipc un; + + rk_pidfile(NULL); + + svc = krb5_config_get_string(context, NULL, + app_string ? app_string : "kdc", + "ipc_csr_authorizer", "service", NULL); + if (svc == NULL) + svc = "org.h5l.csr_authorizer"; + + /* `service' is our request handler; `argv' is its callback data */ + ret = heim_sipc_service_unix(svc, service, NULL, &un); + if (ret) + krb5_err(context, 1, ret, + "Could not setup service on Unix domain socket " + "%s/.heim_%s-socket", socket_dir, svc); + + roken_detach_finish(NULL, daemon_child_flag); + + /* Enter the IPC event loop */ + heim_ipc_main(); + return 0; + } + + /* Client mode */ + if (argc < 2) + usage(1); + + /* Parse the given CSR */ + if ((ret = hx509_request_parse(context->hx509ctx, argv[0], &csr))) + krb5_err(context, 1, ret, "Could not parse PKCS#10 CSR from %s", argv[0]); + + /* + * Parse the client principal that we'll pretend is an authenticated client + * principal. + */ + if ((ret = krb5_parse_name(context, argv[1], &princ))) + krb5_err(context, 1, ret, "Could not parse principal %s", argv[1]); + + /* Call the authorizer */ + ret = kdc_authorize_csr(context, app_string, csr, princ); + + if (ret) { + unsigned n = hx509_request_count_unauthorized(csr); + size_t i, k; + int ret2 = 0; + int good = -1; + + /* + * Check partial approval of SANs. + * + * Iterate over the SANs in the request, and for each check if a) it + * was granted, b) it's on the remainder of our argv[]. + */ + for (i = 0; ret2 == 0; i++) { + hx509_san_type san_type; + char *feature = NULL; + char *san = NULL; + int granted; + + ret2 = hx509_request_get_san(csr, i, &san_type, &san); + if (ret2) + break; + + feature = make_feature_argument("san", san_type, san); + + granted = hx509_request_san_authorized_p(csr, i); + for (k = 2; k < argc; k++) { + if (strcmp(feature, argv[k]) != 0) + continue; + + /* The SAN is on our command line */ + if (granted && good == -1) + good = 1; + else if (!granted) + good = 0; + break; + } + + hx509_xfree(san); + } + + /* Check partial approval of EKUs */ + for (i = 0; ret2 == 0; i++) { + char *feature = NULL; + char *eku = NULL; + int granted; + + ret2 = hx509_request_get_eku(csr, i, &eku); + if (ret2) + break; + + feature = make_feature_argument("eku", 0, eku); + + granted = hx509_request_eku_authorized_p(csr, i); + for (k = 2; k < argc; k++) { + if (strcmp(feature, argv[k]) != 0) + continue; + + /* The SAN is on our command line */ + if (granted && good == -1) + good = 1; + else if (!granted) + good = 0; + break; + } + + hx509_xfree(eku); + } + + if (good != 1) { + krb5_free_principal(context, princ); + _krb5_unload_plugins(context, "kdc"); + hx509_request_free(&csr); + krb5_err(context, 1, ret, + "Authorization failed with %u rejected features", n); + } + } + + printf("Authorized!\n"); + krb5_free_principal(context, princ); + _krb5_unload_plugins(context, "kdc"); + krb5_free_context(context); + hx509_request_free(&csr); + return 0; +} + +/* + * string_encode_sz() and string_encode() encode a string to be safe for use as + * a file name. They function very much like URL encoders, but '~' also gets + * encoded, and '@', '-', '_', and non-leading '.' do not. + * + * A corresponding decoder is not needed. + */ +static size_t +string_encode_sz(const char *in) +{ + size_t sz = strlen(in); + int first = 1; + + while (*in) { + char c = *(in++); + + switch (c) { + case '@': + case '-': + case '_': + break; + case '.': + if (first) + sz += 2; + break; + default: + if (!isalnum((unsigned char)c)) + sz += 2; + } + first = 0; + } + return sz; +} + +static char * +string_encode(const char *in) +{ + size_t len = strlen(in); + size_t sz = string_encode_sz(in); + size_t i, k; + char *s; + int first = 1; + + if ((s = malloc(sz + 1)) == NULL) + return NULL; + s[sz] = '\0'; + + for (i = k = 0; i < len; i++, first = 0) { + unsigned char c = ((const unsigned char *)in)[i]; + + switch (c) { + case '@': + case '-': + case '_': + s[k++] = c; + break; + case '.': + if (first) { + s[k++] = '%'; + s[k++] = "0123456789abcdef"[(c&0xff)>>4]; + s[k++] = "0123456789abcdef"[(c&0x0f)]; + } else { + s[k++] = c; + } + break; + default: + if (isalnum(c)) { + s[k++] = c; + } else { + s[k++] = '%'; + s[k++] = "0123456789abcdef"[(c&0xff)>>4]; + s[k++] = "0123456789abcdef"[(c&0x0f)]; + } + } + } + return s; +} + +static int +stat_authz(const char *subject, + const char *thing) +{ + struct stat st; + char *p = NULL; + int ret; + + if (authz_dir == NULL) + return KRB5_PLUGIN_NO_HANDLE; + if (thing) + ret = asprintf(&p, "%s/%s/%s", authz_dir, subject, thing); + else + ret = asprintf(&p, "%s/%s", authz_dir, subject); + if (ret == -1 || p == NULL) + return ENOMEM; + ret = stat(p, &st); + free(p); + return ret == 0 ? 0 : errno; +} diff --git a/third_party/heimdal/kdc/test_kdc_ca.c b/third_party/heimdal/kdc/test_kdc_ca.c new file mode 100644 index 0000000..4d80c96 --- /dev/null +++ b/third_party/heimdal/kdc/test_kdc_ca.c @@ -0,0 +1,178 @@ +#include "kdc_locl.h" + +static int authorized_flag; +static int help_flag; +static char *lifetime_string; +static const char *app_string = "kdc"; +static int version_flag; + +struct getargs args[] = { + { "authorized", 'A', arg_flag, &authorized_flag, + "Assume CSR is authorized", NULL }, + { "lifetime", 'l', arg_string, &lifetime_string, + "Certificate lifetime desired", "TIME" }, + { "help", 'h', arg_flag, &help_flag, + "Print usage message", NULL }, + { "app", 'a', arg_string, &app_string, + "Application name (kdc or bx509); default: kdc", "APPNAME" }, + { "version", 'v', arg_flag, &version_flag, + "Print version", NULL } +}; +size_t num_args = sizeof(args) / sizeof(args[0]); + +static int +usage(int e) +{ + arg_printusage(args, num_args, NULL, + "PRINC PKCS10:/path/to/der/CSR [HX509-STORE]"); + fprintf(stderr, + "\n\tTest kx509/bx509 online CA issuer functionality.\n" + "\n\tIf --authorized / -A not given, then authorizer plugins\n" + "\twill be invoked.\n" + "\n\tUse --app kdc to test the kx509 configuration.\n" + "\tUse --app bx509 to test the bx509 configuration.\n\n\t" + "Example: %s foo@TEST.H5L.SE PKCS10:/tmp/csr PEM-FILE:/tmp/cert\n", + getprogname()); + exit(e); + return e; +} + +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 + + _krb5_load_plugins(context, "kdc", (const char **)dirs); + +#ifndef _WIN32 + krb5_config_free_strings(cfdirs); +#endif +} + +int +main(int argc, char **argv) +{ + krb5_log_facility *logf = NULL; + krb5_error_code ret; + krb5_principal p = NULL; + krb5_context context; + krb5_times t; + hx509_request req = NULL; + hx509_certs store = NULL; + hx509_certs certs = NULL; + const char *argv0 = argv[0]; + const char *out = "MEMORY:junk-it"; + time_t req_life = 0; + int optidx = 0; + + setprogname(argv[0]); + if (getarg(args, num_args, argc, argv, &optidx)) + return usage(1); + if (help_flag) + return usage(0); + if (version_flag) { + print_version(argv[0]); + return 0; + } + + argc -= optidx; + argv += optidx; + + if (argc < 3 || argc > 4) + usage(1); + + if ((errno = krb5_init_context(&context))) + err(1, "Could not initialize krb5_context"); + if ((ret = krb5_initlog(context, argv0, &logf)) || + (ret = krb5_addlog_dest(context, logf, "0-5/STDERR"))) + krb5_err(context, 1, ret, "Could not set up logging to stderr"); + load_plugins(context); + if ((ret = krb5_parse_name(context, argv[0], &p))) + krb5_err(context, 1, ret, "Could not parse principal %s", argv[0]); + if ((ret = hx509_request_parse(context->hx509ctx, argv[1], &req))) + krb5_err(context, 1, ret, "Could not parse PKCS#10 CSR from %s", argv[1]); + + if (authorized_flag) { + KeyUsage ku = int2KeyUsage(0); + size_t i; + char *s; + + /* Mark all the things authorized */ + ku.digitalSignature = 1; + hx509_request_authorize_ku(req, ku); + + for (i = 0; ret == 0; i++) { + ret = hx509_request_get_eku(req, i, &s); + free(s); s = NULL; + if (ret == 0) + hx509_request_authorize_eku(req, i); + } + if (ret == HX509_NO_ITEM) + ret = 0; + + for (i = 0; ret == 0; i++) { + hx509_san_type san_type; + + ret = hx509_request_get_san(req, i, &san_type, &s); + free(s); s = NULL; + if (ret == 0) + hx509_request_authorize_san(req, i); + } + if (ret && ret != HX509_NO_ITEM) + krb5_err(context, 1, ret, + "Failed to mark requested extensions authorized"); + } else if ((ret = kdc_authorize_csr(context, app_string, req, p))) { + krb5_err(context, 1, ret, + "Requested certificate extensions rejected by policy"); + } + + memset(&t, 0, sizeof(t)); + t.starttime = time(NULL); + t.endtime = t.starttime + 3600; + req_life = lifetime_string ? parse_time(lifetime_string, "day") : 0; + if ((ret = kdc_issue_certificate(context, app_string, logf, req, p, &t, + req_life, 1, &certs))) + krb5_err(context, 1, ret, "Certificate issuance failed"); + + if (argv[2]) + out = argv[2]; + + if ((ret = hx509_certs_init(context->hx509ctx, out, HX509_CERTS_CREATE, + NULL, &store)) || + (ret = hx509_certs_merge(context->hx509ctx, store, certs)) || + (ret = hx509_certs_store(context->hx509ctx, store, 0, NULL))) + /* + * If the store is a MEMORY store, say, we're really not being asked to + * store -- we're just testing the online CA functionality without + * wanting to inspect the result. + */ + if (ret != HX509_UNSUPPORTED_OPERATION) + krb5_err(context, 1, ret, + "Could not store certificate and chain in %s", out); + _krb5_unload_plugins(context, "kdc"); + krb5_free_principal(context, p); + krb5_free_context(context); + hx509_request_free(&req); + hx509_certs_free(&store); + hx509_certs_free(&certs); + return 0; +} diff --git a/third_party/heimdal/kdc/test_token_validator.c b/third_party/heimdal/kdc/test_token_validator.c new file mode 100644 index 0000000..2e4e9dc --- /dev/null +++ b/third_party/heimdal/kdc/test_token_validator.c @@ -0,0 +1,121 @@ +#include "kdc_locl.h" + +static int help_flag; +static int version_flag; +static char *realm; +static char *app; +static struct getarg_strings audiences; + +struct getargs args[] = { + { "app", 'A', arg_string, &app, + "app name (krb5.conf section)", "APP-NAME" }, + { "help", 'h', arg_flag, &help_flag, + "Print usage message", NULL }, + { NULL, 'r', arg_string, &realm, + "Realm name for plugin configuration", "REALM" }, + { NULL, 'a', arg_strings, &audiences, + "expected token acceptor audience (hostname)", "ACCEPTOR-HOSTNAME" }, + { "version", 'v', arg_flag, &version_flag, "Print version", NULL } +}; +size_t num_args = sizeof(args) / sizeof(args[0]); + +static int +usage(int e) +{ + arg_printusage(args, num_args, NULL, "TOKEN-TYPE TOKEN"); + exit(e); + return e; +} + +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 + + _krb5_load_plugins(context, "kdc", (const char **)dirs); + +#ifndef _WIN32 + krb5_config_free_strings(cfdirs); +#endif +} + +int +main(int argc, char **argv) +{ + krb5_error_code ret; + krb5_context context; + krb5_data token; + const char *token_type; + krb5_principal actual_princ = NULL; + krb5_times token_times; + size_t bufsz = 0; + char *buf = NULL; + char *s = NULL; + int optidx = 0; + + setprogname(argv[0]); + if (getarg(args, num_args, argc, argv, &optidx)) + return usage(1); + if (help_flag) + return usage(0); + if (version_flag) { + print_version(argv[0]); + return 0; + } + + argc -= optidx; + argv += optidx; + + if (argc != 2) + usage(1); + + if (krb5_init_context(&context)) + err(1, "Could not initialize krb5_context"); + + load_plugins(context); + + token_type = argv[0]; + token.data = argv[1]; + if (strcmp(token.data, "-") == 0) { + if (getline(&buf, &bufsz, stdin) < 0) + err(1, "Could not read token from stdin"); + token.length = bufsz; + token.data = buf; + } else { + token.length = strlen(token.data); + } + if ((ret = kdc_validate_token(context, realm, token_type, &token, + (const char * const *)audiences.strings, + audiences.num_strings, &actual_princ, + &token_times))) + krb5_err(context, 1, ret, "Could not validate %s token", token_type); + if (actual_princ && (ret = krb5_unparse_name(context, actual_princ, &s))) + krb5_err(context, 1, ret, "Could not display principal name"); + if (s) + printf("Token is valid. Actual principal: %s\n", s); + else + printf("Token is valid."); + _krb5_unload_plugins(context, "kdc"); + krb5_free_principal(context, actual_princ); + krb5_free_context(context); + return 0; +} diff --git a/third_party/heimdal/kdc/token_validator.c b/third_party/heimdal/kdc/token_validator.c new file mode 100644 index 0000000..858fdfa --- /dev/null +++ b/third_party/heimdal/kdc/token_validator.c @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2019 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. + */ + +#include "kdc_locl.h" +#include "token_validator_plugin.h" + +struct plctx { + const char *realm; + const char *token_kind; + krb5_data *token; + const char * const *audiences; + size_t naudiences; + krb5_boolean result; + krb5_principal actual_principal; + krb5_times token_times; +}; + +static krb5_error_code KRB5_LIB_CALL +plcallback(krb5_context context, const void *plug, void *plugctx, void *userctx) +{ + const krb5plugin_token_validator_ftable *validator = plug; + krb5_error_code ret; + struct plctx *plctx = userctx; + + ret = validator->validate(plugctx, context, plctx->realm, + plctx->token_kind, plctx->token, + plctx->audiences, plctx->naudiences, + &plctx->result, &plctx->actual_principal, + &plctx->token_times); + if (ret) { + krb5_free_principal(context, plctx->actual_principal); + plctx->actual_principal = NULL; + } + return ret; +} + +static const char *plugin_deps[] = { "krb5", NULL }; + +static struct heim_plugin_data token_validator_data = { + "kdc", + KDC_PLUGIN_BEARER, + 1, + plugin_deps, + krb5_get_instance +}; + +/* + * Invoke a plugin to validate a JWT/SAML/OIDC token and partially-evaluate + * access control. + */ +KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL +kdc_validate_token(krb5_context context, + const char *realm, + const char *token_kind, + krb5_data *token, + const char * const *audiences, + size_t naudiences, + krb5_principal *actual_principal, + krb5_times *token_times) +{ + krb5_error_code ret; + struct plctx ctx; + + memset(&ctx, 0, sizeof(ctx)); + ctx.realm = realm; + ctx.token_kind = token_kind; + ctx.token = token; + ctx.audiences = audiences; + ctx.naudiences = naudiences; + ctx.result = FALSE; + ctx.actual_principal = NULL; + + krb5_clear_error_message(context); + ret = _krb5_plugin_run_f(context, &token_validator_data, 0, &ctx, + plcallback); + if (ret == 0 && ctx.result && actual_principal) { + *actual_principal = ctx.actual_principal; + ctx.actual_principal = NULL; + } + + if (token_times) + *token_times = ctx.token_times; + + krb5_free_principal(context, ctx.actual_principal); + if (ret) + krb5_prepend_error_message(context, ret, "bearer token validation " + "failed: "); + else if (!ctx.result) + krb5_set_error_message(context, ret = EACCES, + "bearer token validation failed"); + return ret; +} diff --git a/third_party/heimdal/kdc/token_validator_plugin.h b/third_party/heimdal/kdc/token_validator_plugin.h new file mode 100644 index 0000000..8ff0014 --- /dev/null +++ b/third_party/heimdal/kdc/token_validator_plugin.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019 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. + */ + +#ifndef HEIMDAL_KDC_BEARER_TOKEN_PLUGIN_H +#define HEIMDAL_KDC_BEARER_TOKEN_PLUGIN_H 1 + +#define KDC_PLUGIN_BEARER "kdc_token_validator" +#define KDC_PLUGIN_BEARER_VERSION_0 0 + +/* + * @param init Plugin initialization function (see krb5-plugin(7)) + * @param minor_version The plugin minor version number (0) + * @param fini Plugin finalization function + * @param validate Plugin token validation function + * + * The validate field is the plugin entry point that performs the bearer token + * validation operation however the plugin desires. It is invoked in no + * particular order relative to other bearer token validator plugins. The + * plugin validate function must return KRB5_PLUGIN_NO_HANDLE if the rule is + * not applicable to it. + * + * The plugin validate function has the following arguments, in this + * order: + * + * -# plug_ctx, the context value output by the plugin's init function + * -# context, a krb5_context + * -# realm, a const char * + * -# token_type, a const char * + * -# token, a krb5_data * + * -# audiences, a const pointer to an array of const char * containing + * expected audiences of the token (aka, acceptor names) + * -# naudiences, a size_t count of audiences + * -# requested_principal, a krb5_const_principal + * -# validation result, a pointer to a krb5_boolean + * -# actual principal, a krb5_principal * output parameter (optional) + * + * @ingroup krb5_support + */ +typedef struct krb5plugin_token_validator_ftable_desc { + HEIM_PLUGIN_FTABLE_COMMON_ELEMENTS(krb5_context); + krb5_error_code (KRB5_LIB_CALL *validate)(void *, /*plug_ctx*/ + krb5_context, + const char *, /*realm*/ + const char *, /*token_type*/ + krb5_data *, /*token*/ + const char * const *, /*audiences*/ + size_t, /*naudiences*/ + krb5_boolean *, /*valid*/ + krb5_principal *, /*actual_principal*/ + krb5_times *); /*token_times*/ +} krb5plugin_token_validator_ftable; + +#endif /* HEIMDAL_KDC_BEARER_TOKEN_PLUGIN_H */ diff --git a/third_party/heimdal/kdc/version-script.map b/third_party/heimdal/kdc/version-script.map new file mode 100644 index 0000000..c644b30 --- /dev/null +++ b/third_party/heimdal/kdc/version-script.map @@ -0,0 +1,113 @@ +# $Id$ + +HEIMDAL_KDC_1.0 { + global: + kdc_authorize_csr; + kdc_get_instance; + kdc_issue_certificate; + kdc_log; + kdc_log_msg; + kdc_log_msg_va; + kdc_openlog; + kdc_check_flags; + kdc_validate_token; + krb5_kdc_plugin_init; + krb5_kdc_get_config; + krb5_kdc_get_time; + krb5_kdc_pkinit_config; + krb5_kdc_set_dbinfo; + krb5_kdc_process_krb5_request; + krb5_kdc_process_request; + krb5_kdc_save_request; + krb5_kdc_update_time; + krb5_kdc_pk_initialize; + kdc_request_set_attribute; + kdc_request_get_attribute; + kdc_request_copy_attribute; + kdc_request_delete_attribute; + kdc_request_add_encrypted_padata; + kdc_request_add_pac_buffer; + kdc_request_add_reply_padata; + kdc_request_get_addr; + kdc_request_get_armor_client; + kdc_request_get_armor_clientdb; + kdc_request_get_armor_pac; + kdc_request_get_armor_server; + kdc_request_get_canon_client_princ; + kdc_request_get_client; + kdc_request_get_clientdb; + kdc_request_get_client_princ; + kdc_request_get_context; + kdc_request_get_config; + kdc_request_get_cname; + kdc_request_get_error_code; + kdc_request_get_explicit_armor_pac; + kdc_request_get_explicit_armor_clientdb; + kdc_request_get_explicit_armor_client; + kdc_request_get_explicit_armor_present; + kdc_request_get_explicit_armor_server; + kdc_request_get_from; + kdc_request_get_krbtgt; + kdc_request_get_krbtgtdb; + kdc_request_get_krbtgt_princ; + kdc_request_get_pac; + kdc_request_get_pac_attributes; + kdc_request_get_rep; + kdc_request_get_reply_key; + kdc_request_get_req; + kdc_request_get_request; + kdc_request_get_server; + kdc_request_get_serverdb; + kdc_request_get_server_princ; + kdc_request_get_sname; + kdc_request_get_ticket; + kdc_request_get_tv_end; + kdc_request_get_tv_start; + kdc_request_set_canon_client_princ; + kdc_request_set_client_princ; + kdc_request_set_cname; + kdc_request_set_e_data; + kdc_request_set_error_code; + kdc_request_set_krbtgt_princ; + kdc_request_set_pac; + kdc_request_set_pac_attributes; + kdc_request_set_rep; + kdc_request_set_reply_key; + kdc_request_set_server_princ; + kdc_request_set_sname; + kdc_audit_addkv; + kdc_audit_addkv_number; + kdc_audit_addkv_object; + kdc_audit_addkv_timediff; + kdc_audit_addaddrs; + kdc_audit_addreason; + kdc_audit_getkv; + kdc_audit_setkv_bool; + kdc_audit_setkv_number; + kdc_audit_setkv_object; + kdc_audit_vaddkv; + kdc_audit_vaddreason; + _kdc_audit_trail; + + kdc_object_alloc; + kdc_object_retain; + kdc_object_release; + kdc_bool_create; + kdc_bool_get_value; + kdc_array_iterate; + kdc_array_get_length; + kdc_array_get_value; + kdc_array_copy_value; + kdc_string_create; + kdc_string_get_utf8; + kdc_data_create; + kdc_data_get_data; + kdc_number_create; + kdc_number_get_value; + + # needed for digest-service + _kdc_db_fetch; + _kdc_free_ent; + local: + *; +}; |