diff --git a/.travis.yml b/.travis.yml index c2e3ba4d4..e00616a3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ before_install: - if [ $TRAVIS_OS_NAME = linux ]; then sudo apt-get update -qq; fi - if [ $TRAVIS_OS_NAME = linux ]; then sudo apt-get install -qq bison comerr-dev flex libcap-ng-dev libdb-dev libedit-dev libjson-perl libldap2-dev libncurses5-dev libperl4-corelibs-perl libsqlite3-dev libkeyutils-dev pkg-config python ss-dev texinfo unzip netbase keyutils; fi - if [ $TRAVIS_OS_NAME = linux ]; then sudo apt-get install -qq ldap-utils gdb; fi + - if [ $TRAVIS_OS_NAME = linux ]; then sudo apt-get install -qq libmicrohttpd-dev; fi - if [ $TRAVIS_OS_NAME = osx ]; then brew update; fi - if [ $TRAVIS_OS_NAME = osx ]; then brew install cpanm bison flex berkeley-db lmdb openldap openssl; fi - if [ $TRAVIS_OS_NAME = osx ]; then sudo cpanm install JSON; fi diff --git a/README b/README index d2c4eba8c..f10f6a35c 100644 --- a/README +++ b/README @@ -1,5 +1,5 @@ -Heimdal is a Kerberos 5 implementation. +Heimdal is an implementation of: ASN.1/DER, PKIX, and Kerberos. For information how to install see . diff --git a/README.md b/README.md index b56fbd368..3eef367d2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,11 @@ Heimdal ======= -Heimdal is a Kerberos 5 implementation. +Heimdal is an implementation of: + + - ASN.1/DER, + - PKIX, and + - Kerberos. For information how to install see [here](http://www.h5l.org/compile.html). diff --git a/configure.ac b/configure.ac index d07fd8a01..cd8502f6a 100644 --- a/configure.ac +++ b/configure.ac @@ -182,6 +182,54 @@ AM_CONDITIONAL([HAVE_CAPNG], [test "$with_capng" != "no"]) AC_SUBST([CAPNG_CFLAGS]) AC_SUBST([CAPNG_LIBS]) +dnl libmicrohttpd +AC_ARG_WITH([microhttpd], + AC_HELP_STRING([--with-microhttpd], [use microhttpd to serve KDC REST API @<:@default=check@:>@]), + [], + [with_microhttpd=check]) +if test "$with_microhttpd" != "no"; then + PKG_CHECK_MODULES([MICROHTTPD], [libmicrohttpd >= 0.9.59], + [with_microhttpd=yes],[with_microhttpd=no]) +fi +if test "$with_microhttpd" = "yes"; then + AC_DEFINE_UNQUOTED([HAVE_MICROHTTPD], 1, [whether libmicrohttpd is available for KDC REST API]) +fi +AM_CONDITIONAL([HAVE_MICROHTTPD], [test "$with_microhttpd" != "no"]) +AC_SUBST([MICROHTTPD_CFLAGS]) +AC_SUBST([MICROHTTPD_LIBS]) + +dnl libcjwt +AC_ARG_WITH([cjwt], + AC_HELP_STRING([--with-cjwt], [(Experimental) use cjwt to validate JWT tokens @<:@default=check@:>@]), + [], + [with_cjwt=check]) +if test "$with_cjwt" != "no"; then + PKG_CHECK_MODULES([CJWT], [libcjwt >= 1.0.0], + [with_cjwt=yes],[with_cjwt=no]) +fi +if test "$with_cjwt" = "yes"; then + AC_DEFINE_UNQUOTED([HAVE_CJWT], 1, [whether libcjwt is available for KDC REST API]) +fi +AM_CONDITIONAL([HAVE_CJWT], [test "$with_cjwt" != "no"]) +AC_SUBST([CJWT_CFLAGS]) +AC_SUBST([CJWT_LIBS]) + +dnl libcjson +AC_ARG_WITH([cjson], + AC_HELP_STRING([--with-cjson], [(Experimental) use cJSON to extract private claims from JWT tokens @<:@default=check@:>@]), + [], + [with_cjson=check]) +if test "$with_cjson" != "no"; then + PKG_CHECK_MODULES([CJSON], [libcjson >= 1.0.0], + [with_cjson=yes],[with_cjson=no]) +fi +if test "$with_cjson" = "yes"; then + AC_DEFINE_UNQUOTED([HAVE_CJSON], 1, [whether libcjson is available for KDC REST API]) +fi +AM_CONDITIONAL([HAVE_CJSON], [test "$with_cjson" != "no"]) +AC_SUBST([CJSON_CFLAGS]) +AC_SUBST([CJSON_LIBS]) + dnl Check for sqlite rk_TEST_PACKAGE(sqlite3, [#include diff --git a/doc/heimdal.texi b/doc/heimdal.texi index a40da9c3d..131ce6a9c 100644 --- a/doc/heimdal.texi +++ b/doc/heimdal.texi @@ -22,19 +22,20 @@ @ifinfo @dircategory Security @direntry -* Heimdal: (heimdal). The Kerberos 5 distribution from KTH +* Heimdal: (heimdal). The Kerberos 5 and PKIX distribution from KTH @end direntry @end ifinfo @c title page @titlepage @title Heimdal -@subtitle Kerberos 5 from KTH +@subtitle Kerberos 5 and PKIX from KTH @subtitle Edition @value{EDITION}, for version @value{VERSION} @subtitle 2008 @author Johan Danielsson @author Love Hörnquist Åstrand @author Assar Westerlund +@author et al @end titlepage @@ -64,6 +65,8 @@ This manual for version @value{VERSION} of Heimdal. @menu * Introduction:: * What is Kerberos?:: +* What is PKIX?:: +* What is a Certification Authority (CA)?:: * Building and Installing:: * Setting up a realm:: * Applications:: diff --git a/doc/hx509.texi b/doc/hx509.texi index d050c2118..0a90cb735 100644 --- a/doc/hx509.texi +++ b/doc/hx509.texi @@ -48,7 +48,7 @@ @page @copyrightstart -Copyright (c) 1994-2008 Kungliga Tekniska Högskolan +Copyright (c) 1994-2019 Kungliga Tekniska Högskolan (Royal Institute of Technology, Stockholm, Sweden). All rights reserved. @@ -187,7 +187,7 @@ This manual is for version @value{VERSION} of hx509. @menu * Introduction:: -* What is X.509 ?:: +* What are X.509 and PKIX ?:: * Setting up a CA:: * CMS signing and encryption:: * Certificate matching:: @@ -230,13 +230,20 @@ Software PKCS 11 module @end detailmenu @end menu -@node Introduction, What is X.509 ?, Top, Top +@node Introduction, What are X.509 and PKIX ?, Top, Top @chapter Introduction -The goals of a PKI infrastructure (as defined in -RFC 3280) is to meet -@emph{the needs of deterministic, automated identification, authentication, access control, and authorization}. +A Public Key Infrastructure (PKI) is an authentication mechanism based on +entities having certified cryptographic public keys and corresponding private +(secret) keys. +The ITU-T PKI specifications are designated "x.509", while the IETF PKI +specifications (PKIX) are specified by a number of Internet RFCs and are based +on x.509. + +The goals of a PKI (as stated in +RFC 5280) is to meet +@emph{the needs of deterministic, automated identification, authentication, access control, and authorization}. The administrator should be aware of certain terminologies as explained by the aforementioned RFC before attemping to put in place a PKI infrastructure. Briefly, these are: @@ -246,6 +253,9 @@ RFC before attemping to put in place a PKI infrastructure. Briefly, these are: Certificate Authority @item RA Registration Authority, i.e., an optional system to which a CA delegates certain management functions. +@item Certificate +A binary document that names an entity and its public key and which is signed +by an issuing CA. @item CRL Issuer An optional system to which a CA delegates the publication of certificate revocation lists. @item Repository @@ -253,7 +263,7 @@ A system or collection of distributed systems that stores certificates and CRLs and serves as a means of distributing these certificates and CRLs to end entities @end itemize -hx509 (Heimdal x509 support) is a near complete X.509 stack that can +hx509 (Heimdal x509 support) is a near complete X.509/PKIX stack that can handle CMS messages (crypto system used in S/MIME and Kerberos PK-INIT) and basic certificate processing tasks, path construction, path validation, OCSP and CRL validation, PKCS10 message construction, CMS @@ -263,10 +273,13 @@ signed), and CMS EnvelopedData (certificate encrypted). hx509 can use PKCS11 tokens, PKCS12 files, PEM files, and/or DER encoded files. -@node What is X.509 ?, Setting up a CA, Introduction, Top -@chapter What is X.509, PKIX, PKCS7 and CMS ? +hx509 consists of a library (libhx509) and a command-line utility (hxtool), as +well as a RESTful, HTTPS-based service that implements an online CA. -X.509 was created by CCITT (later ITU) for the X.500 directory +@node What are X.509 and PKIX ?, Setting up a CA, Introduction, Top +@chapter What are X.509 and PKIX, PKIX, PKCS7 and CMS ? + +X.509 was created by CCITT (later ITU-T) for the X.500 directory service. Today, X.509 discussions and implementations commonly reference the IETF's PKIX Certificate and CRL Profile of the X.509 v3 certificate standard, as specified in RFC 3280. @@ -348,7 +361,7 @@ The process starts by looking at the issuing CA of the certificate, by Name or Key Identifier, and tries to find that certificate while at the same time evaluting any policies in-place. -@node Setting up a CA, Creating a CA certificate, What is X.509 ?, Top +@node Setting up a CA, Creating a CA certificate, What are X.509 and PKIX ?, Top @chapter Setting up a CA Do not let information overload scare you off! If you are simply testing diff --git a/doc/whatis.texi b/doc/whatis.texi index 7d83725d9..be4010a57 100644 --- a/doc/whatis.texi +++ b/doc/whatis.texi @@ -1,6 +1,6 @@ @c $Id$ -@node What is Kerberos?, Building and Installing, Introduction, Top +@node What is Kerberos?, What is PKIX?, Introduction, Top @chapter What is Kerberos? @quotation @@ -162,3 +162,32 @@ from 1988. These documents can be found on our web-page at @url{http://www.pdc.kth.se/kth-krb/}. + +@node What is PKIX?, What is a Certification Authority (CA)?, Introduction, Top +@chapter What is PKIX? + +PKIX is the set of Internet standards for Public Key Infrastructure (PKI), +based on the ITU-T's x.509 standads. PKI is an authentication mechanism based +on public keys (the 'PK' in 'PKI'). + +In PKIX we have public keys "certified" by certification authorities (CAs). A +"relying party" is software that validates an entity's certificate and, if +valid, trusts the certified public key to "speak for" the entity identified by +the certificate. + +In a PKI every entity has one (or more) certified public/private key pairs. + +@node What is a Certification Authority (CA)?, Building and Installing, Introduction, Top + +A Certification Authority (CA) is an entity in a PKI that issues certificates +to other entities -- a CA certifies that a public key speaks for a particular, +named entity. + +There are two types of CAs: off-line and online. Typically PKI hierarchies are +organized such that the most security-critical private keys are only used by +off-line CAs to certify the less security-critical public keys of online CAs. + +Heimdal has support for off-line CAs using its Hx509 library and hxtool +command. + +Heimdal also has an online CA with a RESTful, HTTPS-based protocol. diff --git a/kdc/Makefile.am b/kdc/Makefile.am index 842b5a888..7097e307d 100644 --- a/kdc/Makefile.am +++ b/kdc/Makefile.am @@ -4,13 +4,17 @@ include $(top_srcdir)/Makefile.am.common AM_CPPFLAGS += $(INCLUDE_libintl) $(INCLUDE_openssl_crypto) -I$(srcdir)/../lib/krb5 -lib_LTLIBRARIES = libkdc.la +lib_LTLIBRARIES = simple_csr_authorizer.la \ + ipc_csr_authorizer.la \ + libkdc.la cjwt_token_validator.la \ + negotiate_token_validator.la bin_PROGRAMS = string2key sbin_PROGRAMS = kstash -libexec_PROGRAMS = hprop hpropd kdc digest-service +libexec_PROGRAMS = hprop hpropd kdc digest-service \ + test_token_validator test_csr_authorizer test_kdc_ca noinst_PROGRAMS = kdc-replay kdc-tester @@ -23,6 +27,21 @@ 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 \ + libkdc.la \ + $(MICROHTTPD_LIBS) \ + $(LIB_roken) \ + $(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 +endif + digest_service_SOURCES = \ digest-service.c @@ -35,8 +54,29 @@ 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) +cjwt_token_validator_la_SOURCES = cjwt_token_validator.c +cjwt_token_validator_la_AM_CPPFLAGS = $(CJSON_FLAGS) $(CJWT_FLAGS) +cjwt_token_validator_la_LDFLAGS = -module $(CJSON_LIBS) $(CJWT_LIBS) +negotiate_token_validator_la_SOURCES = negotiate_token_validator.c +negotiate_token_validator_la_LDFLAGS = -module $(top_builddir)/lib/gssapi/libgssapi.la +# CSR Authorizer plugins (for kdc/kx509 and bx509d) +simple_csr_authorizer_la_SOURCES = simple_csr_authorizer.c +simple_csr_authorizer_la_LDFLAGS = -module +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 + libkdc_la_SOURCES = \ default_config.c \ + ca.c \ set_dbinfo.c \ digest.c \ fast.c \ @@ -48,6 +88,8 @@ libkdc_la_SOURCES = \ log.c \ misc.c \ kx509.c \ + token_validator.c \ + csr_authorizer.c \ process.c \ windc.c \ rx.h @@ -57,6 +99,9 @@ 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) @@ -135,13 +180,16 @@ digest_service_LDADD = \ $(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) +test_csr_authorizer_LDADD = libkdc.la $(top_builddir)/lib/hx509/libhx509.la $(LDADD) $(LIB_pidfile) $(LIB_heimbase) +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 = windc_plugin.h +krb5_HEADERS = windc_plugin.h token_validator_plugin.h csr_authorizer_plugin.h build_HEADERZ = $(krb5_HEADERS) # XXX diff --git a/kdc/NTMakefile b/kdc/NTMakefile index c4bc69ab1..cb20d26aa 100644 --- a/kdc/NTMakefile +++ b/kdc/NTMakefile @@ -92,17 +92,21 @@ $(LIBEXECDIR)\kdc.exe: \ LIBKDC_OBJS=\ $(OBJ)\default_config.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)\log.obj \ - $(OBJ)\misc.obj \ - $(OBJ)\kx509.obj \ - $(OBJ)\process.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)\log.obj \ + $(OBJ)\misc.obj \ + $(OBJ)\kx509.obj \ + $(OBJ)\token_validator.obj \ + $(OBJ)\csr_authorizer.obj \ + $(OBJ)\process.obj \ $(OBJ)\windc.obj LIBKDC_LIBS=\ @@ -126,9 +130,10 @@ clean:: libkdc_la_SOURCES = \ default_config.c \ + ca.c \ set_dbinfo.c \ digest.c \ - fast.c \ + fast.c \ kdc_locl.h \ kerberos5.c \ krb5tgs.c \ @@ -137,6 +142,8 @@ libkdc_la_SOURCES = \ log.c \ misc.c \ kx509.c \ + token_validator.c \ + csr_authorizer.c \ process.c \ windc.c \ rx.h diff --git a/kdc/bx509d.c b/kdc/bx509d.c new file mode 100644 index 000000000..63139d651 --- /dev/null +++ b/kdc/bx509d.c @@ -0,0 +1,1722 @@ +/* + * 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. + * + * Users are authenticated with bearer tokens. + * + * This is essentially a RESTful online CA sharing code with the KDC's kx509 + * online CA, and also a proxy for PKINIT and GSS-API (Negotiate). + * + * To get a key certified: + * + * GET /bx509?csr= + * + * To get an HTTP/Negotiate token: + * + * GET /bnegotiate?target= + * + * which, if authorized, produces a Negotiate token (base64-encoded, as + * expected, with the "Negotiate " prefix, ready to be put in an Authorization: + * header). + * + * TBD: + * - rewrite to not use libmicrohttpd but an alternative more appropriate to + * Heimdal's license (though libmicrohttpd will do) + * - /bx509 should include the certificate chain + * - /bx509 should support HTTP/Negotiate + * - there should be an end-point for fetching an issuer's chain + * - maybe add /bkrb5 which returns a KRB-CRED with the user's TGT + * + * 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.) + */ + +#define _XOPEN_SOURCE_EXTENDED 1 +#define _DEFAULT_SOURCE 1 +#define _BSD_SOURCE 1 +#define _GNU_SOURCE 1 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "kdc_locl.h" +#include "token_validator_plugin.h" +#include +#include +#include +#include +#include +#include +#include "../lib/hx509/hx_locl.h" +#include + +static krb5_kdc_configuration *kdc_config; +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; + (void) pthread_setspecific(k5ctx, *contextp); + return *contextp ? 0 : ENOMEM; +} + +static int port = -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 const char *cert_file; +static const char *priv_key_file; +static const char *cache_dir; +static char *impersonation_key_fn; + +static krb5_error_code resp(struct MHD_Connection *, int, + enum MHD_ResponseMemoryMode, const void *, size_t, + const char *); +static krb5_error_code bad_req(struct MHD_Connection *, krb5_error_code, int, + const char *, ...) + HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5)); + +static krb5_error_code bad_reqv(struct MHD_Connection *, krb5_error_code, int, + const char *, va_list) + HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 0)); +static krb5_error_code bad_enomem(struct MHD_Connection *, krb5_error_code); +static krb5_error_code bad_400(struct MHD_Connection *, krb5_error_code, char *); +static krb5_error_code bad_401(struct MHD_Connection *, char *); +static krb5_error_code bad_403(struct MHD_Connection *, krb5_error_code, char *); +static krb5_error_code bad_404(struct MHD_Connection *, const char *); +static krb5_error_code bad_405(struct MHD_Connection *, const char *); +static krb5_error_code bad_500(struct MHD_Connection *, krb5_error_code, const char *); +static krb5_error_code bad_503(struct MHD_Connection *, krb5_error_code, const char *); + +static int +validate_token(struct MHD_Connection *connection, + krb5_times *token_times, + char **cprinc_from_token) +{ + krb5_error_code ret; + krb5_principal actual_cprinc = NULL; + krb5_context context; + const char *token; + const char *host; + char token_type[64]; /* Plenty */ + char *p; + krb5_data tok; + size_t host_len, brk, i; + + memset(token_times, 0, sizeof(*token_times)); + ret = get_krb5_context(&context); + if (ret) + return bad_500(connection, ret, + "Could not set up context for token validation"); + + host = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, + MHD_HTTP_HEADER_HOST); + if (host == NULL) + return bad_400(connection, ret, "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(connection, MHD_HEADER_KIND, + MHD_HTTP_HEADER_AUTHORIZATION); + if (token == NULL) + return bad_401(connection, "Authorization token is missing"); + brk = strcspn(token, " \t"); + if (token[brk] == '\0' || brk > sizeof(token_type) - 1) + return bad_401(connection, "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(connection, EINVAL, "Host: value is not accepted here"); + + ret = kdc_validate_token(context, NULL /* realm */, token_type, &tok, + (const char **)&audiences.strings[i], 1, + &actual_cprinc, token_times); + if (ret) + return bad_403(connection, ret, "Token validation failed"); + if (actual_cprinc == NULL) + return bad_403(connection, ret, "Could not extract a principal name " + "from token"); + ret = krb5_unparse_name(context, actual_cprinc, + cprinc_from_token); + krb5_free_principal(context, actual_cprinc); + 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")) + 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; + + *pkix_store = NULL; + if (asprintf(&s, "PEM-FILE:%s/pkix-XXXXXX", cache_dir) == -1 || + s == NULL) { + free(s); + return ret; + } + /* + * This way of using mkstemp() isn't safer than mktemp(), but we want to + * quiet the warning that we'd get if we used mktemp(). + */ + if ((fd = mkstemp(s + sizeof("PEM-FILE:") - 1)) == -1) { + free(s); + return errno; + } + (void) close(fd); + *pkix_store = s; + return 0; +} + +/* + * XXX Shouldn't be a body, but a status message. The body should be + * configurable to be from a file. MHD doesn't give us a way to set the + * response status message though, just the body. + */ +static krb5_error_code +resp(struct MHD_Connection *connection, + int http_status_code, + enum MHD_ResponseMemoryMode rmmode, + const void *body, + size_t bodylen, + const char *token) +{ + struct MHD_Response *response; + int mret = MHD_YES; + + response = MHD_create_response_from_buffer(bodylen, rk_UNCONST(body), + rmmode); + if (response == NULL) + return -1; + if (http_status_code == MHD_HTTP_UNAUTHORIZED) { + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_WWW_AUTHENTICATE, + "Bearer"); + if (mret == MHD_YES) + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_WWW_AUTHENTICATE, + "Negotiate"); + } else if (http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) { + const char *redir; + + redir = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, + "redirect"); + mret = MHD_add_response_header(response, MHD_HTTP_HEADER_LOCATION, + redir); + if (mret != MHD_NO && token) + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_AUTHORIZATION, + token); + } + if (mret != MHD_NO) + mret = MHD_queue_response(connection, http_status_code, response); + MHD_destroy_response(response); + return mret == MHD_NO ? -1 : 0; +} + +static krb5_error_code +bad_req(struct MHD_Connection *connection, + 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(connection, code, http_status_code, fmt, ap); + va_end(ap); + return ret; +} + +static krb5_error_code +bad_reqv(struct MHD_Connection *connection, + 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; + + get_krb5_context(&context); + + if (code == ENOMEM) { + if (context) + kdc_log(context, kdc_config, 4, "Out of memory"); + return resp(connection, http_status_code, MHD_RESPMEM_PERSISTENT, + fmt, strlen(fmt), 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; + } + krb5_free_error_message(context, k5msg); + + if (ret == -1 || msg == NULL) { + if (context) + kdc_log(context, kdc_config, 4, "Out of memory"); + return resp(connection, MHD_HTTP_SERVICE_UNAVAILABLE, + MHD_RESPMEM_PERSISTENT, + "Out of memory", sizeof("Out of memory") - 1, NULL); + } + + if (http_status_code == MHD_HTTP_OK) + kdc_log(context, kdc_config, 4, "HTTP Response status code %d", http_status_code); + else + kdc_log(context, kdc_config, 4, "HTTP Response status code %d: %s", http_status_code, msg); + ret = resp(connection, http_status_code, MHD_RESPMEM_MUST_COPY, + msg, strlen(msg), NULL); + free(formatted); + free(msg); + return ret == -1 ? -1 : code; +} + +static krb5_error_code +bad_enomem(struct MHD_Connection *connection, krb5_error_code ret) +{ + return bad_req(connection, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Out of memory"); +} + +static krb5_error_code +bad_400(struct MHD_Connection *connection, int ret, char *reason) +{ + return bad_req(connection, ret, MHD_HTTP_BAD_REQUEST, "%s", reason); +} + +static krb5_error_code +bad_401(struct MHD_Connection *connection, char *reason) +{ + return bad_req(connection, EACCES, MHD_HTTP_UNAUTHORIZED, "%s", reason); +} + +static krb5_error_code +bad_403(struct MHD_Connection *connection, krb5_error_code ret, char *reason) +{ + return bad_req(connection, EACCES, MHD_HTTP_FORBIDDEN, "%s", reason); +} + +static krb5_error_code +bad_404(struct MHD_Connection *connection, const char *name) +{ + return bad_req(connection, ENOENT, MHD_HTTP_NOT_FOUND, + "Resource not found: %s", name); +} + +static krb5_error_code +bad_405(struct MHD_Connection *connection, const char *method) +{ + return bad_req(connection, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED, + "Method not supported: %s", method); +} + +static krb5_error_code +bad_500(struct MHD_Connection *connection, + krb5_error_code ret, + const char *reason) +{ + return bad_req(connection, ret, MHD_HTTP_INTERNAL_SERVER_ERROR, + "Internal error: %s", reason); +} + +static krb5_error_code +bad_503(struct MHD_Connection *connection, + krb5_error_code ret, + const char *reason) +{ + return bad_req(connection, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Service unavailable: %s", reason); +} + +static krb5_error_code +good_bx509(struct MHD_Connection *connection, + const char *pkix_store) +{ + krb5_error_code ret; + size_t bodylen; + void *body; + + ret = rk_undumpdata(strchr(pkix_store, ':') + 1, &body, &bodylen); + if (ret) + return bad_503(connection, ret, "Could not recover issued certificate " + "from PKIX store"); + + ret = resp(connection, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, body, bodylen, + NULL); + free(body); + return ret; +} + +struct bx509_param_handler_arg { + krb5_context context; + hx509_request req; + krb5_error_code ret; +}; + +static int +bx509_param_cb(void *d, + enum MHD_ValueKind kind, + const char *key, + const char *val) +{ + struct bx509_param_handler_arg *a = d; + heim_oid oid = { 0, 0 }; + + if (strcmp(key, "eku") == 0 && val) { + + a->ret = der_parse_heim_oid(val, ".", &oid); + if (a->ret == 0) + a->ret = hx509_request_add_eku(a->context->hx509ctx, a->req, &oid); + der_free_oid(&oid); + } else if (strcmp(key, "dNSName") == 0 && val) { + a->ret = hx509_request_add_dns_name(a->context->hx509ctx, a->req, val); + } else if (strcmp(key, "rfc822Name") == 0 && val) { + a->ret = hx509_request_add_email(a->context->hx509ctx, a->req, val); + } else if (strcmp(key, "xMPPName") == 0 && val) { + a->ret = hx509_request_add_xmpp_name(a->context->hx509ctx, a->req, + val); + } else if (strcmp(key, "krb5PrincipalName") == 0 && val) { + a->ret = hx509_request_add_pkinit(a->context->hx509ctx, a->req, val); + } else if (strcmp(key, "ms-upn") == 0 && val) { + a->ret = hx509_request_add_ms_upn_name(a->context->hx509ctx, a->req, + val); + } else if (strcmp(key, "registeredID") == 0 && val) { + a->ret = der_parse_heim_oid(val, ".", &oid); + if (a->ret == 0) + a->ret = hx509_request_add_registered(a->context->hx509ctx, a->req, + &oid); + der_free_oid(&oid); + } else if (strcmp(key, "csr") == 0 && val) { + a->ret = 0; /* Handled upstairs */ + } else { + /* Produce error for unknown params */ + krb5_set_error_message(a->context, a->ret = ENOTSUP, + "Query parameter %s not supported", key); + } + return a->ret == 0 ? MHD_YES : MHD_NO /* Stop iterating */; +} + +static krb5_error_code +update_and_authorize_CSR(krb5_context context, + struct MHD_Connection *connection, + krb5_data *csr, + krb5_const_principal p, + hx509_request *req) +{ + struct bx509_param_handler_arg cb_data; + krb5_error_code ret; + + *req = NULL; + + ret = hx509_request_parse_der(context->hx509ctx, csr, req); + if (ret) + return bad_req(connection, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not parse CSR"); + cb_data.context = context; + cb_data.req = *req; + cb_data.ret = 0; + (void) MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, + bx509_param_cb, &cb_data); + ret = cb_data.ret; + if (ret) { + hx509_request_free(req); + return bad_req(connection, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not handle query parameters"); + } + + ret = kdc_authorize_csr(context, kdc_config, *req, p); + if (ret) { + hx509_request_free(req); + return bad_403(connection, ret, + "Not authorized to requested certificate"); + } + return ret; +} + +static krb5_error_code +store_certs(hx509_context context, const char *store, hx509_certs store_these) +{ + krb5_error_code ret; + hx509_certs certs = NULL; + + ret = hx509_certs_init(context, store, HX509_CERTS_CREATE, NULL, + &certs); + if (ret == 0) + 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(krb5_context context, + struct MHD_Connection *connection, + const char *csr, + const char *princ, + krb5_times *token_times, + char **pkix_store) +{ + krb5_error_code ret = 0; + krb5_principal p; + hx509_request req = NULL; + hx509_certs certs = NULL; + krb5_data d; + ssize_t bytes; + + *pkix_store = NULL; + + ret = krb5_parse_name(context, princ, &p); + if (ret) + return bad_req(connection, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not parse principal name"); + + /* Set CSR */ + if ((d.data = malloc(strlen(csr))) == NULL) { + krb5_free_principal(context, p); + return bad_enomem(connection, ENOMEM); + } + + bytes = rk_base64_decode(csr, d.data); + if (bytes < 0) + ret = errno; + else + d.length = bytes; + if (ret) { + krb5_free_principal(context, p); + free(d.data); + return bad_500(connection, ret, "Invalid base64 encoding of CSR"); + } + + /* + * Parses and validates the CSR, adds external extension requests from + * query parameters, then checks authorization. + */ + ret = update_and_authorize_CSR(context, connection, &d, p, &req); + free(d.data); + d.data = 0; + d.length = 0; + if (ret) { + krb5_free_principal(context, p); + return ret; /* update_and_authorize_CSR() calls bad_req() */ + } + + /* Issue the certificate */ + ret = kdc_issue_certificate(context, kdc_config, req, p, token_times, + 1 /* send_chain */, &certs); + krb5_free_principal(context, p); + hx509_request_free(&req); + if (ret) { + if (ret == KRB5KDC_ERR_POLICY || ret == EACCES) + return bad_403(connection, ret, + "Certificate request denied for policy reasons"); + return bad_500(connection, ret, "Certificate issuance failed"); + } + + /* Setup PKIX store */ + if ((ret = mk_pkix_store(pkix_store))) + return bad_500(connection, ret, + "Could not create PEM store for issued certificate"); + + ret = store_certs(context->hx509ctx, *pkix_store, certs); + hx509_certs_free(&certs); + if (ret) { + (void) unlink(strchr(*pkix_store, ':') + 1); + free(*pkix_store); + *pkix_store = NULL; + return bad_500(connection, ret, + "Failed convert issued certificate and chain to PEM"); + } + return 0; +} + +/* Implements GETs of /bx509 */ +static krb5_error_code +bx509(struct MHD_Connection *connection) +{ + krb5_error_code ret; + krb5_context context; + krb5_times token_times; + const char *csr; + char *cprinc_from_token = NULL; + char *pkix_store = NULL; + + if ((ret = get_krb5_context(&context))) + return bad_503(connection, ret, "Could not initialize Kerberos " + "library"); + + /* Get required inputs */ + csr = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, + "csr"); + if (csr == NULL) + return bad_400(connection, EINVAL, "CSR is missing"); + + if ((ret = validate_token(connection, &token_times, &cprinc_from_token))) + return ret; /* validate_token() calls bad_req() */ + + if (cprinc_from_token == NULL) + return bad_403(connection, EINVAL, + "Could not extract principal name from token"); + + /* Parse CSR, add extensions from parameters, authorize, issue cert */ + if ((ret = do_CA(context, connection, csr, cprinc_from_token, + &token_times, &pkix_store))) { + free(cprinc_from_token); + return ret; + } + + /* Read and send the contents of the PKIX store */ + kdc_log(context, kdc_config, 4, "Issued certificate to %s", + cprinc_from_token); + ret = good_bx509(connection, pkix_store); + + if (pkix_store) + (void) unlink(strchr(pkix_store, ':') + 1); + free(cprinc_from_token); + free(pkix_store); + return ret == -1 ? MHD_NO : MHD_YES; +} + +/* + * 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. + */ +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(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) + 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) + return 0; + if (cc) + krb5_cc_close(context, cc); + 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(krb5_context context, + const char *princ, + const char *pkix_store, + const char *ccname) +{ + krb5_get_init_creds_opt *opt = NULL; + krb5_init_creds_context ctx = NULL; + krb5_error_code ret = ENOMEM; + krb5_ccache temp_cc = NULL; + krb5_ccache cc = NULL; + krb5_principal p = NULL; + struct stat st1, st2; + time_t life; + const char *crealm; + const char *fn = NULL; + char *temp_ccname = NULL; + int fd = -1; + + if ((ret = krb5_cc_resolve(context, ccname, &cc))) + return ret; + + /* + * Avoid nasty race conditions and ccache file corruption, take an flock on + * temp_ccname and do the cleanup dance. + */ + if (asprintf(&temp_ccname, "%s.ccnew", ccname) == -1 || + temp_ccname == NULL) + ret = ENOMEM; + if (ret == 0) + fn = temp_ccname + sizeof("FILE:") - 1; + if (ret == 0) do { + /* + * Open and flock the 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; + 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(context, temp_ccname, &temp_cc); + if (ret == 0) + ret = krb5_cc_get_lifetime(context, cc, &life); + if (ret == 0 && life > 60) + goto out; /* We lost the race, we get to do less work */ + + /* + * We won the race. 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. + */ + ret = krb5_parse_name(context, princ, &p); + if (ret == 0) + crealm = krb5_principal_get_realm(context, p); + if (ret == 0 && + (ret = krb5_cc_initialize(context, temp_cc, p)) == 0 && + (ret = krb5_get_init_creds_opt_alloc(context, &opt)) == 0) + krb5_get_init_creds_opt_set_default_flags(context, "kinit", crealm, + opt); + if (ret == 0 && + (ret = krb5_get_init_creds_opt_set_addressless(context, + opt, 1)) == 0) + ret = krb5_get_init_creds_opt_set_pkinit(context, opt, p, pkix_store, + NULL, /* XXX pkinit_anchor */ + NULL, /* XXX anchor_chain */ + NULL, /* XXX pkinit_crl */ + 0, /* flags */ + NULL, /* prompter */ + NULL, /* prompter data */ + NULL /* password */); + if (ret == 0) + ret = krb5_init_creds_init(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. + */ + if (ret == 0 && + (ret = krb5_init_creds_get(context, ctx)) == 0 && + (ret = krb5_init_creds_store(context, ctx, temp_cc)) == 0 && + (ret = krb5_cc_move(context, temp_cc, cc)) == 0) + temp_cc = NULL; + +out: + if (ctx) + krb5_init_creds_free(context, ctx); + krb5_get_init_creds_opt_free(context, opt); + krb5_free_principal(context, p); + if (temp_cc) + krb5_cc_close(context, temp_cc); + if (cc) + krb5_cc_close(context, cc); + if (fd != -1) + (void) close(fd); /* Drops the flock */ + 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 +bnegotiate_do_CA(krb5_context context, + struct MHD_Connection *connection, + const char *princ, + krb5_times *token_times, + char **pkix_store) +{ + 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); + + *pkix_store = NULL; + memset(&spki, 0, sizeof(spki)); + ku.digitalSignature = 1; + + /* Make a CSR (halfway -- we don't need to sign it here) */ + ret = load_priv_key(context, impersonation_key_fn, &key); + if (ret == 0) + ret = hx509_request_init(context->hx509ctx, &req); + if (ret == 0) + ret = krb5_parse_name(context, princ, &p); + if (ret == 0) + hx509_private_key2SPKI(context->hx509ctx, key, &spki); + if (ret == 0) + ret = hx509_request_add_pkinit(context->hx509ctx, req, princ); + if (ret == 0) + ret = hx509_request_add_eku(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(context, kdc_config, req, p, token_times, + 1 /* send_chain */, &certs); + hx509_private_key_free(&key); + krb5_free_principal(context, p); + hx509_request_free(&req); + p = NULL; + + if (ret == KRB5KDC_ERR_POLICY) + return bad_500(connection, ret, + "Certificate request denied for policy reasons"); + if (ret == ENOMEM) + return bad_503(connection, ret, "Certificate issuance failed"); + if (ret) + return bad_500(connection, ret, "Certificate issuance failed"); + + /* Setup PKIX store and extract the certificate chain into it */ + ret = mk_pkix_store(pkix_store); + if (ret == 0) + ret = store_certs(context->hx509ctx, *pkix_store, certs); + hx509_certs_free(&certs); + if (ret) { + (void) unlink(strchr(*pkix_store, ':') + 1); + free(*pkix_store); + *pkix_store = NULL; + return bad_500(connection, ret, + "Could not create PEM store for issued certificate"); + } + return 0; +} + +/* Get impersonated Kerberos credentials for `cprinc' */ +static krb5_error_code +bnegotiate_get_creds(struct MHD_Connection *connection, + const char *subject_cprinc, + krb5_times *token_times, + char **ccname) +{ + krb5_error_code ret; + krb5_context context; + char *pkix_store = NULL; + + *ccname = NULL; + + if ((ret = get_krb5_context(&context))) + return bad_503(connection, ret, "Could not initialize Kerberos " + "library"); + + /* If we have a live ccache for `cprinc', we're done */ + if ((ret = find_ccache(context, subject_cprinc, 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 = bnegotiate_do_CA(context, connection, subject_cprinc, + token_times, &pkix_store))) + return ret; /* bnegotiate_do_CA() calls bad_req() */ + + if (ret == 0 && + (ret = do_pkinit(context, subject_cprinc, pkix_store, *ccname))) + ret = bad_403(connection, ret, + "Could not acquire PKIX credentials using PKINIT"); + + free(pkix_store); + return ret; +} + +/* Accumulate strings */ +static void +acc_str(char **acc, char *adds, size_t addslen) +{ + char *tmp; + 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 ? r : "Out of memory while formatting GSS-API error"; +} + +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, 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 MHD_Connection *connection, + 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; + + ret = resp(connection, http_status_code, MHD_RESPMEM_MUST_COPY, + msg, strlen(msg), NULL); + free(msg); + return ret; +} + +static gss_OID +get_name_type(struct MHD_Connection *connection) +{ + const char *nt; + + nt = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, + "nametype"); + if (nt == NULL || strcmp(nt, "hostbased-service") == 0) + return GSS_C_NT_HOSTBASED_SERVICE; + if (strcmp(nt, "exported-name") == 0) + return GSS_C_NT_EXPORT_NAME; + if (strcmp(nt, "krb5") == 0) + return GSS_KRB5_NT_PRINCIPAL_NAME; + return GSS_C_NO_OID; +} + +/* Make an HTTP/Negotiate token */ +static krb5_error_code +mk_nego_tok(struct MHD_Connection *connection, + const char *cprinc, + const char *target, + const char *ccname, + char **nego_tok, + size_t *nego_toksz) +{ + gss_key_value_element_desc kv[1] = { { "ccache", 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; + gss_OID nt; + krb5_error_code ret; /* More like a system error code here */ + char *token_b64 = NULL; + + *nego_tok = NULL; + *nego_toksz = 0; + if ((nt = get_name_type(connection)) == GSS_C_NO_OID) + return bad_400(connection, EINVAL, "unknown GSS name type in request"); + + /* Import initiator name */ + name.length = strlen(cprinc); + name.value = rk_UNCONST(cprinc); + major = gss_import_name(&minor, &name, GSS_KRB5_NT_PRINCIPAL_NAME, &iname); + if (major != GSS_S_COMPLETE) + return bad_req_gss(connection, 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(target); + name.value = rk_UNCONST(target); + major = gss_import_name(&minor, &name, nt, &aname); + if (major != GSS_S_COMPLETE) { + (void) gss_release_name(&junk, &iname); + return bad_req_gss(connection, 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(connection, 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_release_name(&junk, &aname); + (void) gss_release_cred(&junk, &cred); + if (major != GSS_S_COMPLETE) + return bad_req_gss(connection, 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(connection, errno, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not allocate memory for encoding Negotiate " + "token"); + *nego_toksz = ret; + return 0; +} + +/* + * Implements /bnegotiate end-point. + * + * Query parameters: + * + * - target= (REQUIRED) + * - nametype=hostbased-service|exported-name|krb5 (OPTIONAL) + * - redirect= (OPTIONAL) + */ +static krb5_error_code +bnegotiate(struct MHD_Connection *connection) +{ + krb5_error_code ret; + krb5_times token_times; + const char *target; + const char *redir; + size_t nego_toksz = 0; + char *nego_tok = NULL; + char *cprinc_from_token = NULL; + char *ccname = NULL; + + target = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, + "target"); + if (target == NULL) + return bad_400(connection, EINVAL, + "Query missing 'target' parameter value"); + redir = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, + "redirect"); + + if ((ret = validate_token(connection, &token_times, &cprinc_from_token))) + return ret; /* validate_token() calls bad_req() */ + + if (cprinc_from_token == NULL) + return bad_400(connection, EINVAL, + "Could not extract principal name from token"); + + /* + * 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 = bnegotiate_get_creds(connection, cprinc_from_token, &token_times, + &ccname); + + /* Acquire the Negotiate token and output it */ + if (ret == 0 && ccname != NULL) + ret = mk_nego_tok(connection, cprinc_from_token, target, ccname, + &nego_tok, &nego_toksz); + + if (ret == 0) { + /* Look ma', Negotiate as an OAuth-like token system! */ + if (redir) + ret = resp(connection, MHD_HTTP_TEMPORARY_REDIRECT, + MHD_RESPMEM_PERSISTENT, "", 0, nego_tok); + else + ret = resp(connection, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, + nego_tok, nego_toksz, NULL); + } + + free(cprinc_from_token); + free(nego_tok); + free(ccname); + return ret == -1 ? MHD_NO : MHD_YES; +} + +/* Implements the entirety of this REST service */ +static int +route(void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **ctx) +{ + static int aptr = 0; + + if (0 != strcmp(method, "GET")) + return bad_405(connection, method) == -1 ? MHD_NO : MHD_YES; + + if (*ctx == NULL) { + /* + * This is the first call, right after headers were read. + * + * We must return quickly so that any 100-Continue might be sent with + * celerity. + * + * We'll get called again to really do the processing. If we handled + * POSTs then we'd also get called with upload_data != NULL between the + * first and last calls. We need to keep no state between the first + * and last calls, but we do need to distinguish first and last call, + * so we use the ctx argument for this. + */ + *ctx = &aptr; + return MHD_YES; + } + if (strcmp(url, "/bx509") == 0) + return bx509(connection); + if (strcmp(url, "/bnegotiate") == 0) + return bnegotiate(connection); + return bad_404(connection, url) == -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 bx509 service", "HOSTNAME" }, + { "daemon", 'd', arg_flag, &daemonize, "daemonize", "daemonize" }, + { "daemon-child", 0, arg_flag, &daemon_child_fd, NULL, NULL }, /* priv */ + { "reverse-proxied", 0, arg_flag, &reverse_proxied_flag, + "reverse proxied", "listen on 127.0.0.1 and do not use TLS" }, + { NULL, 'p', arg_integer, &port, "PORT", "port number (default: 443)" }, + { "cache-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", "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 /bx509 and /bnegotiate,\n" + "performing corresponding kx509 and, possibly, PKINIT requests\n" + "to the KDCs of the requested 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) + ; +} + +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 (audiences.num_strings == 0) { + char localhost[MAXHOSTNAMELEN]; + + ret = gethostname(localhost, sizeof(localhost)); + if (ret == -1) + errx(1, "Could not determine local hostname; use --audience"); + + if ((audiences.strings = + calloc(1, sizeof(audiences.strings[0]))) == NULL || + (audiences.strings[0] = strdup(localhost)) == NULL) + err(1, "Out of memory"); + audiences.num_strings = 1; + } + + if (daemonize && daemon_child_fd == -1) + daemon_child_fd = roken_detach_prep(argc, argv, "--daemon-child"); + daemonize = 0; + + argc -= optidx; + argv += optidx; + + if ((errno = pthread_key_create(&k5ctx, k5_free_context))) + err(1, "Could not create thread-specific storage"); + + if ((errno = get_krb5_context(&context))) + err(1, "Could not init krb5 context"); + + if ((ret = krb5_kdc_get_config(context, &kdc_config))) + krb5_err(context, 1, ret, "Could not init krb5 context"); + + kdc_openlog(context, "bx509d", kdc_config); + kdc_config->app = "bx509"; + + 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, + NULL, NULL, + route, (char *)NULL, + MHD_OPTION_SOCK_ADDR, &sin, + MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200, + MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10, + MHD_OPTION_END); + } else if (sock != MHD_INVALID_SOCKET) { + /* + * Certificate/key rollover: reuse the listen socket returned by + * MHD_quiesce_daemon(). + */ + current = MHD_start_daemon(flags | MHD_USE_SSL, port, + NULL, NULL, + route, (char *)NULL, + MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem, + MHD_OPTION_HTTPS_MEM_CERT, cert_pem, + MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200, + MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10, + MHD_OPTION_LISTEN_SOCKET, sock, + MHD_OPTION_END); + sock = MHD_INVALID_SOCKET; + } else { + current = MHD_start_daemon(flags | MHD_USE_SSL, port, + NULL, NULL, + route, (char *)NULL, + MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem, + MHD_OPTION_HTTPS_MEM_CERT, cert_pem, + MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200, + MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10, + MHD_OPTION_END); + } + if (current == NULL) + err(1, "Could not start 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); + pthread_key_delete(k5ctx); + return 0; +} diff --git a/kdc/ca.c b/kdc/ca.c new file mode 100644 index 000000000..b68d25ad4 --- /dev/null +++ b/kdc/ca.c @@ -0,0 +1,727 @@ +/* + * 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 +#include +#include +#include + +#include + +/* + * 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. + */ + +typedef enum { + CERT_NOTSUP = 0, + CERT_CLIENT = 1, + CERT_SERVER = 2, + CERT_MIXED = 3 +} cert_type; + +static void +frees(char **s) +{ + free(*s); + *s = NULL; +} + +static krb5_error_code +count_sans(hx509_request req, size_t *n) +{ + size_t i; + char *s = NULL; + int ret = 0; + + *n = 0; + for (i = 0; ret == 0; i++) { + hx509_san_type san_type; + + frees(&s); + ret = hx509_request_get_san(req, i, &san_type, &s); + if (ret) + break; + switch (san_type) { + case HX509_SAN_TYPE_DNSNAME: + case HX509_SAN_TYPE_EMAIL: + case HX509_SAN_TYPE_XMPP: + case HX509_SAN_TYPE_PKINIT: + case HX509_SAN_TYPE_MS_UPN: + (*n)++; + break; + default: + ret = ENOTSUP; + } + frees(&s); + } + return ret == HX509_NO_ITEM ? 0 : ret; +} + +static int +has_sans(hx509_request req) +{ + hx509_san_type san_type; + char *s = NULL; + int ret = hx509_request_get_san(req, 0, &san_type, &s); + + frees(&s); + return ret == HX509_NO_ITEM ? 0 : 1; +} + +static cert_type +characterize_cprinc(krb5_context context, + krb5_principal cprinc) +{ + unsigned int ncomp = krb5_principal_get_num_comp(context, cprinc); + const char *comp1 = krb5_principal_get_comp_string(context, cprinc, 1); + + switch (ncomp) { + case 1: + return CERT_CLIENT; + case 2: + if (strchr(comp1, '.') == NULL) + return CERT_CLIENT; + return CERT_SERVER; + case 3: + if (strchr(comp1, '.')) + return CERT_SERVER; + return CERT_NOTSUP; + default: + return CERT_NOTSUP; + } +} + +/* Characterize request as client or server cert req */ +static cert_type +characterize(krb5_context context, + krb5_principal cprinc, + hx509_request req) +{ + krb5_error_code ret = 0; + cert_type res = CERT_NOTSUP; + size_t i; + char *s = NULL; + int want_ekus = 0; + + if (!has_sans(req)) + return characterize_cprinc(context, cprinc); + + for (i = 0; ret == 0; i++) { + heim_oid oid; + + frees(&s); + ret = hx509_request_get_eku(req, i, &s); + if (ret) + break; + + want_ekus = 1; + ret = der_parse_heim_oid(s, ".", &oid); + if (ret) + break; + /* + * If the client wants only a server certificate, then we'll be + * willing to issue one that may be longer-lived than the client's + * ticket/token. + * + * There may be other server EKUs, but these are the ones we know + * of. + */ + if (der_heim_oid_cmp(&asn1_oid_id_pkix_kp_serverAuth, &oid) && + der_heim_oid_cmp(&asn1_oid_id_pkix_kp_OCSPSigning, &oid) && + der_heim_oid_cmp(&asn1_oid_id_pkix_kp_secureShellServer, &oid)) + res |= CERT_CLIENT; + else + res |= CERT_SERVER; + der_free_oid(&oid); + } + frees(&s); + if (ret == HX509_NO_ITEM) + ret = 0; + + for (i = 0; ret == 0; i++) { + hx509_san_type san_type; + + frees(&s); + ret = hx509_request_get_san(req, i, &san_type, &s); + if (ret) + break; + switch (san_type) { + case HX509_SAN_TYPE_DNSNAME: + if (!want_ekus) + res |= CERT_SERVER; + break; + case HX509_SAN_TYPE_EMAIL: + case HX509_SAN_TYPE_XMPP: + case HX509_SAN_TYPE_PKINIT: + case HX509_SAN_TYPE_MS_UPN: + if (!want_ekus) + res |= CERT_CLIENT; + break; + default: + ret = ENOTSUP; + } + if (ret) + break; + } + frees(&s); + if (ret == HX509_NO_ITEM) + ret = 0; + return ret ? CERT_NOTSUP : res; +} + +/* + * 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 const krb5_config_binding * +get_cf(krb5_context context, + const char *toplevel, + hx509_request req, + krb5_principal cprinc) +{ + krb5_error_code ret; + const krb5_config_binding *cf = NULL; + unsigned int ncomp = krb5_principal_get_num_comp(context, cprinc); + const char *realm = krb5_principal_get_realm(context, cprinc); + const char *comp0 = krb5_principal_get_comp_string(context, cprinc, 0); + const char *comp1 = krb5_principal_get_comp_string(context, cprinc, 1); + const char *label = NULL; + const char *svc = NULL; + const char *def = NULL; + cert_type certtype = CERT_NOTSUP; + size_t nsans = 0; + + if (ncomp == 0) { + krb5_set_error_message(context, ENOTSUP, + "Client principal has no components!"); + return NULL; + } + + if ((ret = count_sans(req, &nsans)) || + (certtype = characterize(context, cprinc, req)) == CERT_NOTSUP) { + krb5_set_error_message(context, ret, + "Could not characterize CSR"); + return NULL; + } + + if (nsans) { + def = "custom"; + /* Client requested some certificate extension, a SAN or EKU */ + switch (certtype) { + case CERT_MIXED: label = "mixed"; break; + case CERT_CLIENT: label = "client"; break; + case CERT_SERVER: label = "server"; break; + default: return NULL; + } + } else { + def = "default"; + /* Default certificate desired */ + if (ncomp == 1) { + label = "user"; + } else if (ncomp == 2 && strcmp(comp1, "root") == 0) { + label = "root_user"; + } else if (ncomp == 2 && strcmp(comp1, "admin") == 0) { + label = "admin_user"; + } else if (strchr(comp1, '.')) { + label = "hostbased_service"; + svc = comp0; + } else { + label = "other"; + } + } + + if (strcmp(toplevel, "kdc") == 0) + cf = krb5_config_get_list(context, NULL, toplevel, "realms", realm, + "kx509", label, svc, NULL); + else + cf = krb5_config_get_list(context, NULL, toplevel, "realms", realm, + label, svc, NULL); + if (cf == NULL) + krb5_set_error_message(context, ENOTSUP, + "No %s configuration for %s %s certificates [%s] realm " + "-> %s -> kx509 -> %s%s%s", + strcmp(toplevel, "bx509") == 0 ? "bx509" : "kx509", + def, label, toplevel, realm, label, + svc ? " -> " : "", svc ? svc : ""); + return cf; +} + +/* + * Find and set a certificate template using a configuration sub-tree + * appropriate to the requesting principal. + * + * This allows for the specification of the following in configuration: + * + * - certificates as templates, with ${var} tokens in subjectName attribute + * values that will be expanded later + * - a plain string with ${var} tokens to use as the subjectName + * - EKUs + * - whether to include a PKINIT SAN + */ +static krb5_error_code +set_template(krb5_context context, + const krb5_config_binding *cf, + hx509_ca_tbs tbs) +{ + krb5_error_code ret = 0; + const char *cert_template = NULL; + const char *subj_name = NULL; + char **ekus = NULL; + + if (cf == NULL) + return KRB5KDC_ERR_POLICY; /* Can't happen */ + + cert_template = krb5_config_get_string(context, cf, "template_cert", NULL); + subj_name = krb5_config_get_string(context, cf, "subject_name", NULL); + ekus = krb5_config_get_strings(context, cf, "ekus", NULL); + + if (cert_template) { + hx509_certs certs; + hx509_cert template; + + ret = hx509_certs_init(context->hx509ctx, cert_template, 0, + NULL, &certs); + if (ret == 0) + ret = hx509_get_one_cert(context->hx509ctx, certs, &template); + hx509_certs_free(&certs); + if (ret) { + krb5_set_error_message(context, KRB5KDC_ERR_POLICY, + "Failed to load certificate template from " + "%s", cert_template); + return ret; + } + + /* + * Only take the subjectName, the keyUsage, and EKUs from the template + * certificate. + */ + ret = hx509_ca_tbs_set_template(context->hx509ctx, tbs, + HX509_CA_TEMPLATE_SUBJECT | + HX509_CA_TEMPLATE_KU | + HX509_CA_TEMPLATE_EKU, + template); + hx509_cert_free(template); + if (ret) + return ret; + } + + if (subj_name) { + hx509_name dn = NULL; + + ret = hx509_parse_name(context->hx509ctx, subj_name, &dn); + if (ret == 0) + ret = hx509_ca_tbs_set_subject(context->hx509ctx, tbs, dn); + hx509_name_free(&dn); + if (ret) + return ret; + } + + if (cert_template == NULL && subj_name == NULL) { + hx509_name dn = NULL; + + ret = hx509_empty_name(context->hx509ctx, &dn); + if (ret == 0) + ret = hx509_ca_tbs_set_subject(context->hx509ctx, tbs, dn); + hx509_name_free(&dn); + if (ret) + return ret; + } + + if (ekus) { + size_t i; + + for (i = 0; ret == 0 && ekus[i]; i++) { + heim_oid oid = { 0, 0 }; + + if ((ret = der_find_or_parse_heim_oid(ekus[i], ".", &oid)) == 0) + ret = hx509_ca_tbs_add_eku(context->hx509ctx, tbs, &oid); + der_free_oid(&oid); + } + krb5_config_free_strings(ekus); + } + + /* + * XXX A KeyUsage template would be nice, but it needs some smarts to + * remove, e.g., encipherOnly, decipherOnly, keyEncipherment, if the SPKI + * algorithm does not support encryption. The same logic should be added + * to hx509_ca_tbs_set_template()'s HX509_CA_TEMPLATE_KU functionality. + */ + return ret; +} + +/* + * Find and set a certificate template, set "variables" in `env', and add add + * default SANs/EKUs as appropriate. + * + * TODO: + * - lookup a 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) + */ +static krb5_error_code +set_tbs(krb5_context context, + const krb5_config_binding *cf, + hx509_request req, + krb5_principal cprinc, + hx509_env *env, + hx509_ca_tbs tbs) +{ + krb5_error_code ret; + unsigned int ncomp = krb5_principal_get_num_comp(context, cprinc); + const char *realm = krb5_principal_get_realm(context, cprinc); + const char *comp0 = krb5_principal_get_comp_string(context, cprinc, 0); + const char *comp1 = krb5_principal_get_comp_string(context, cprinc, 1); + const char *comp2 = krb5_principal_get_comp_string(context, cprinc, 2); + char *princ_no_realm = NULL; + char *princ = NULL; + + ret = krb5_unparse_name_flags(context, cprinc, 0, &princ); + if (ret == 0) + ret = krb5_unparse_name_flags(context, cprinc, + KRB5_PRINCIPAL_UNPARSE_NO_REALM, + &princ_no_realm); + if (ret == 0) + ret = hx509_env_add(context->hx509ctx, env, + "principal-name-without-realm", princ_no_realm); + if (ret == 0) + ret = hx509_env_add(context->hx509ctx, env, "principal-name", princ); + if (ret == 0) + ret = hx509_env_add(context->hx509ctx, env, "principal-name-realm", + realm); + + /* Populate requested certificate extensions from CSR/CSRPlus if allowed */ + ret = hx509_ca_tbs_set_from_csr(context->hx509ctx, tbs, req); + if (ret == 0) + ret = set_template(context, cf, tbs); + + /* + * Optionally add PKINIT SAN. + * + * Adding an id-pkinit-san means the client can use the certificate to + * initiate PKINIT. That might seem odd, but it enables a sort of PKIX + * credential delegation by allowing forwarded Kerberos tickets to be + * used to acquire PKIX credentials. Thus this can work: + * + * PKIX (w/ HW token) -> Kerberos -> + * PKIX (w/ softtoken) -> Kerberos -> + * PKIX (w/ softtoken) -> Kerberos -> + * ... + * + * Note that we may not have added the PKINIT EKU -- that depends on the + * template, and host-based service templates might well not include it. + */ + if (ret == 0 && !has_sans(req) && + krb5_config_get_bool_default(context, cf, FALSE, "include_pkinit_san", + NULL)) { + ret = hx509_ca_tbs_add_san_pkinit(context->hx509ctx, tbs, princ); + } + + if (ret) + goto out; + + if (ncomp == 1) { + const char *email_domain; + + ret = hx509_env_add(context->hx509ctx, env, "principal-component0", + princ_no_realm); + + /* + * If configured, include an rfc822Name that's just the client's + * principal name sans realm @ configured email domain. + */ + if (ret == 0 && !has_sans(req) && + (email_domain = krb5_config_get_string(context, cf, "email_domain", + NULL))) { + char *email; + + if (asprintf(&email, "%s@%s", princ_no_realm, email_domain) == -1 || + email == NULL) + goto enomem; + ret = hx509_ca_tbs_add_san_rfc822name(context->hx509ctx, tbs, email); + free(email); + } + goto out; + } else if (ncomp == 2 || ncomp == 3) { + /* + * 2- and 3-component principal name. + * + * We do not have a reliable name-type indicator. If the second + * component has a '.' in it then we'll assume that the name is a + * host-based (2-component) or domain-based (3-component) service + * principal name. Else we'll assume it's a two-component admin-style + * username. + */ + + ret = hx509_env_add(context->hx509ctx, env, "principal-component0", + comp0); + if (ret == 0) + ret = hx509_env_add(context->hx509ctx, env, "principal-component1", + comp1); + if (ret == 0 && ncomp == 3) + ret = hx509_env_add(context->hx509ctx, env, "principal-component2", + comp2); + + if (ret) + goto out; + + if (ret == 0 && strchr(comp1, '.')) { + /* Looks like host-based or domain-based service */ + ret = hx509_env_add(context->hx509ctx, env, + "principal-service-name", comp0); + if (ret == 0) + ret = hx509_env_add(context->hx509ctx, env, "principal-host-name", comp1); + if (ret == 0 && ncomp == 3) + ret = hx509_env_add(context->hx509ctx, env, "principal-domain-name", comp2); + if (ret == 0 && !has_sans(req) && + krb5_config_get_bool_default(context, cf, FALSE, + "include_dnsname_san", NULL)) { + ret = hx509_ca_tbs_add_san_hostname(context->hx509ctx, tbs, comp1); + } + } + } else { + krb5_set_error_message(context, ret = KRB5KDC_ERR_POLICY, + "kx509/bx509 client %s has too many " + "components!", princ); + } + +out: + krb5_xfree(princ_no_realm); + krb5_xfree(princ); + return ret; + +enomem: + ret = krb5_enomem(context); + goto out; +} + +static krb5_error_code +tbs_set_times(krb5_context context, + const krb5_config_binding *cf, + krb5_times *auth_times, + time_t req_life, + hx509_ca_tbs tbs) +{ + time_t now = time(NULL); + time_t endtime = auth_times->endtime; + time_t starttime = auth_times->starttime ? + auth_times->starttime : now - 5 * 60; + time_t fudge = + krb5_config_get_time_default(context, cf, 5 * 24 * 3600, + "force_cert_lifetime", NULL); + time_t clamp = + krb5_config_get_time_default(context, cf, 0, "max_cert_lifetime", + NULL); + + if (fudge && now + fudge > endtime) + endtime = now + fudge; + + if (req_life && req_life < endtime - now) + endtime = now + req_life; + + if (clamp && clamp < endtime - now) + endtime = now + clamp; + + hx509_ca_tbs_set_notAfter(context->hx509ctx, tbs, endtime); + hx509_ca_tbs_set_notBefore(context->hx509ctx, tbs, starttime); + return 0; +} + +/* + * Build a certifate for `principal' and its CSR. + */ +krb5_error_code +kdc_issue_certificate(krb5_context context, + const krb5_kdc_configuration *config, + hx509_request req, + krb5_principal cprinc, + krb5_times *auth_times, + int send_chain, + hx509_certs *out) +{ + const krb5_config_binding *cf; + krb5_error_code ret; + const char *kx509_ca; + hx509_ca_tbs tbs = NULL; + hx509_certs chain = NULL; + hx509_cert signer = NULL; + hx509_cert cert = NULL; + hx509_env env = NULL; + KeyUsage ku; + + *out = NULL; + /* Force KU */ + ku = int2KeyUsage(0); + ku.digitalSignature = 1; + hx509_request_authorize_ku(req, ku); + + /* Get configuration */ + if ((cf = get_cf(context, config->app, req, cprinc)) == NULL) + return KRB5KDC_ERR_POLICY; + if ((kx509_ca = krb5_config_get_string(context, cf, "ca", NULL)) == NULL) { + krb5_set_error_message(context, ret = KRB5KDC_ERR_POLICY, + "No kx509 CA issuer credential specified"); + return ret; + } + + ret = hx509_ca_tbs_init(context->hx509ctx, &tbs); + if (ret) + return ret; + + /* Lookup a template and set things in `env' and `tbs' as appropriate */ + if (ret == 0) + ret = set_tbs(context, cf, req, cprinc, &env, tbs); + + /* Populate generic template "env" variables */ + + /* + * The `tbs' and `env' are now complete as to naming and EKUs. + * + * We check that the `tbs' is not name-less, after which all remaining + * failures here will not be policy failures. So we also log the intent to + * issue a certificate now. + */ + if (ret == 0 && hx509_name_is_null_p(hx509_ca_tbs_get_name(tbs)) && + !has_sans(req)) + krb5_set_error_message(context, ret = KRB5KDC_ERR_POLICY, + "Not issuing certificate because it " + "would have no names"); + if (ret) + goto out; + + /* + * Still to be done below: + * + * - set certificate spki + * - set certificate validity + * - expand variables in certificate subject name template + * - sign certificate + * - encode certificate and chain + */ + + /* Load the issuer certificate and private key */ + { + hx509_certs certs; + hx509_query *q; + + ret = hx509_certs_init(context->hx509ctx, kx509_ca, 0, NULL, &certs); + if (ret) { + krb5_set_error_message(context, ret, "Failed to load CA %s", + kx509_ca); + goto out; + } + ret = hx509_query_alloc(context->hx509ctx, &q); + if (ret) { + hx509_certs_free(&certs); + goto out; + } + + hx509_query_match_option(q, HX509_QUERY_OPTION_PRIVATE_KEY); + hx509_query_match_option(q, HX509_QUERY_OPTION_KU_KEYCERTSIGN); + + ret = hx509_certs_find(context->hx509ctx, certs, q, &signer); + hx509_query_free(context->hx509ctx, q); + hx509_certs_free(&certs); + if (ret) { + krb5_set_error_message(context, ret, "Failed to find a CA in %s", + kx509_ca); + goto out; + } + } + + /* Populate the subject public key in the TBS context */ + { + SubjectPublicKeyInfo spki; + + ret = hx509_request_get_SubjectPublicKeyInfo(context->hx509ctx, + req, &spki); + if (ret == 0) + ret = hx509_ca_tbs_set_spki(context->hx509ctx, tbs, &spki); + free_SubjectPublicKeyInfo(&spki); + if (ret) + goto out; + } + + /* Work out cert expiration */ + if (ret == 0) + ret = tbs_set_times(context, cf, auth_times, 0 /* XXX req_life */, tbs); + + /* Expand the subjectName template in the TBS using the env */ + if (ret == 0) + ret = hx509_ca_tbs_subject_expand(context->hx509ctx, tbs, env); + hx509_env_free(&env); + + /* All done with the TBS, sign/issue the certificate */ + ret = hx509_ca_sign(context->hx509ctx, tbs, signer, &cert); + if (ret) + goto out; + + /* Gather the certificate and chain into a MEMORY store */ + ret = hx509_certs_init(context->hx509ctx, "MEMORY:certs", 0, NULL, out); + if (ret == 0) + ret = hx509_certs_add(context->hx509ctx, *out, cert); + if (ret == 0 && send_chain) { + ret = hx509_certs_init(context->hx509ctx, kx509_ca, 0, NULL, &chain); + if (ret == 0) + ret = hx509_certs_merge(context->hx509ctx, *out, chain); + } + +out: + hx509_certs_free(&chain); + if (env) + hx509_env_free(&env); + if (tbs) + hx509_ca_tbs_free(&tbs); + if (cert) + hx509_cert_free(cert); + if (signer) + hx509_cert_free(signer); + if (ret) + hx509_certs_free(out); + return ret; +} diff --git a/kdc/cjwt_token_validator.c b/kdc/cjwt_token_validator.c new file mode 100644 index 000000000..49c481111 --- /dev/null +++ b/kdc/cjwt_token_validator.c @@ -0,0 +1,330 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_CJSON +#include +#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 == next->length && + memcmp(current->data, next->data, next->length) == 0) { + free(next->data); + next->data = 0; + next->length = 0; + } + if (current->length == previous->length && + memcmp(current->data, previous->data, previous->length) == 0) { + free(previous->data); + previous->data = 0; + previous->length = 0; + } + 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 ((ret = cjwt_decode(tokstr, 0, &jwt, jwk_current.data, + jwk_current.length)) == -2 && + (ret = cjwt_decode(tokstr, 0, &jwt, jwk_next.data, + jwk_next.length)) == -2) + 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->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(krb5_context 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/kdc/csr_authorizer.c b/kdc/csr_authorizer.c new file mode 100644 index 000000000..f8456daf7 --- /dev/null +++ b/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 { + krb5_kdc_configuration *config; + 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->config, plctx->csr, + plctx->client, &plctx->result); +} + +static const char *plugin_deps[] = { "krb5", NULL }; + +static struct krb5_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. + */ +krb5_error_code +kdc_authorize_csr(krb5_context context, + krb5_kdc_configuration *config, + hx509_request csr, + krb5_const_principal client) +{ + krb5_error_code ret; + struct plctx ctx; + + ctx.config = config; + 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/kdc/csr_authorizer_plugin.h b/kdc/csr_authorizer_plugin.h new file mode 100644 index 000000000..3f9b0651b --- /dev/null +++ b/kdc/csr_authorizer_plugin.h @@ -0,0 +1,76 @@ +/* + * 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 + * -# config, a krb5_kdc_configuration * + * -# 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 { + int minor_version; + krb5_error_code (KRB5_LIB_CALL *init)(krb5_context, void **); + void (KRB5_LIB_CALL *fini)(void *); + krb5_error_code (KRB5_LIB_CALL *authorize)(void *, /*plug_ctx*/ + krb5_context, + krb5_kdc_configuration *, + hx509_request, /*CSR*/ + krb5_const_principal,/*client*/ + krb5_boolean *); /*authorized*/ +} krb5plugin_csr_authorizer_ftable; + +#endif /* HEIMDAL_KDC_CSR_AUTHORIZER_PLUGIN_H */ diff --git a/kdc/default_config.c b/kdc/default_config.c index ea42935dc..b168175f1 100644 --- a/kdc/default_config.c +++ b/kdc/default_config.c @@ -37,17 +37,53 @@ #include #include +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 +} + krb5_error_code 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; + 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; @@ -111,17 +147,7 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config) c->enable_kx509 = krb5_config_get_bool_default(context, NULL, FALSE, - "kdc", "enable-kx509", NULL); - - if (c->enable_kx509) { - /* These are global defaults. There are also per-realm defaults. */ - c->kx509_template = - krb5_config_get_string(context, NULL, - "kdc", "kx509_template", NULL); - c->kx509_ca = - krb5_config_get_string(context, NULL, - "kdc", "kx509_ca", NULL); - } + "kdc", "enable_kx509", NULL); #endif c->tgt_use_strongest_session_key = diff --git a/kdc/ipc_csr_authorizer.c b/kdc/ipc_csr_authorizer.c new file mode 100644 index 000000000..8ca8ec538 --- /dev/null +++ b/kdc/ipc_csr_authorizer.c @@ -0,0 +1,443 @@ +/* + * 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 + * + * + * + * where the is 4 bytes, unsigned binary in network byte order, and + * is an array of bytes and does NOT include a NUL + * terminator. + * + * Requests are of the form: + * + * check = ... + * + * where is a URL-escaped principal name, is one of: + * + * - san_pkinit + * - san_xmpp + * - san_email + * - san_ms_upn + * - san_dnsname + * - eku + * + * and 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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/* + * 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(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; + + 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) + return rk_strpoolfree(*cmd), *cmd = NULL, ENOMEM; + *cmd = rk_strpoolprintf(*cmd, "%s", s); + free(s); + if (*cmd == NULL) + return ENOMEM; + } + return 0; +} + +static int +call_svc(krb5_context context, heim_ipc ipc, const char *cmd) +{ + heim_octet_string req, resp; + int ret; + + req.data = (void *)(uintptr_t)cmd; + req.length = strlen(cmd); + resp.length = 0; + resp.data = NULL; + if ((ret = heim_ipc_call(ipc, &req, &resp, NULL))) { + if (resp.length && resp.length < INT_MAX) { + krb5_set_error_message(context, ret, "CSR denied: %.*s", + (int)resp.length, (const char *)resp.data); + ret = EACCES; + } else { + krb5_set_error_message(context, EACCES, "CSR denied because could " + "not reach CSR authorizer IPC service"); + ret = EACCES; + } + return ret; + } + if (resp.data == NULL || resp.length == 0) { + free(resp.data); + krb5_set_error_message(context, ret, "CSR authorizer IPC service " + "failed silently"); + return EACCES; + } + if (resp.length == sizeof("denied") - 1 && + strncasecmp(resp.data, "denied", sizeof("denied") - 1) == 0) { + free(resp.data); + krb5_set_error_message(context, ret, "CSR authorizer rejected %s", + cmd); + return EACCES; + } + if (resp.length == sizeof("granted") - 1 && + strncasecmp(resp.data, "granted", sizeof("granted") - 1) == 0) { + free(resp.data); + return 0; + } + krb5_set_error_message(context, ret, "CSR authorizer failed %s: %.*s", + cmd, resp.length < INT_MAX ? (int)resp.length : 0, + resp.data); + return EACCES; +} + +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_eku(csr, i); + frees(&s); + } + return ret == HX509_NO_ITEM ? 0 : ret; +} + +static KRB5_LIB_CALL krb5_error_code +authorize(void *ctx, + krb5_context context, + krb5_kdc_configuration *config, + 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; + + if ((svc = krb5_config_get_string(context, NULL, + config->app ? config->app : "kdc", + "ipc_csr_authorizer", "service", + NULL)) == NULL) + return KRB5_PLUGIN_NO_HANDLE; + + if ((ret = heim_ipc_init_context(svc, &ipc))) { + 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; + + ret = hx509_request_get_san(csr, i, &san_type, &s); + if (ret) + break; + 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) { + if ((s = rk_strpoolcollect(cmd)) == NULL) + goto enomem; + cmd = NULL; + if ((ret = call_svc(context, ipc, s))) + goto out; + } /* else -> permit */ + + if ((ret = mark_authorized(csr))) + goto out; + + *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(krb5_context 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/kdc/kdc.h b/kdc/kdc.h index 0910d1e14..9f871a7fd 100644 --- a/kdc/kdc.h +++ b/kdc/kdc.h @@ -93,13 +93,12 @@ typedef struct krb5_kdc_configuration { size_t max_datagram_reply_length; int enable_kx509; - const char *kx509_template; - const char *kx509_ca; krb5_boolean enable_derived_keys; int derived_keys_ndots; int derived_keys_maxdots; + const char *app; } krb5_kdc_configuration; struct krb5_kdc_service { diff --git a/kdc/kx509.c b/kdc/kx509.c index 0068c74da..7ae2992c8 100644 --- a/kdc/kx509.c +++ b/kdc/kx509.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -47,7 +48,8 @@ * * 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. + * 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. @@ -70,6 +72,28 @@ * or * CN=component0,CN=component1,..,CN=componentN,DC= * 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 @@ -89,8 +113,9 @@ typedef struct kx509_req_context { krb5_keyblock *key; hx509_request csr; krb5_data *reply; - unsigned int have_auth_data:1; /* Relevant authz data in the AP-REQ */ - unsigned int send_chain:1; /* Client expects a full chain */ + 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; /* @@ -103,9 +128,9 @@ _kdc_try_kx509_request(void *ptr, size_t len, struct Kx509Request *req) size_t sz; if (len < sizeof(version_2_0)) - return -1; + return -1; if (memcmp(version_2_0, p, sizeof(version_2_0)) != 0) - return -1; + return -1; p += sizeof(version_2_0); len -= sizeof(version_2_0); if (len == 0) @@ -114,8 +139,10 @@ _kdc_try_kx509_request(void *ptr, size_t len, struct Kx509Request *req) } static krb5_boolean -get_bool_param(krb5_context context, krb5_boolean def, - const char *crealm, const char *name) +get_bool_param(krb5_context context, + krb5_boolean def, + const char *crealm, + const char *name) { krb5_boolean global_default; @@ -132,25 +159,25 @@ get_bool_param(krb5_context context, krb5_boolean def, */ static krb5_error_code verify_req_hash(krb5_context context, - const Kx509Request *req, - krb5_keyblock *key) + 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 have wrong length: %lu", - (unsigned long)req->pk_hash.length); - return KRB5KDC_ERR_PREAUTH_FAILED; + 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); HMAC_Init_ex(&ctx, - key->keyvalue.data, key->keyvalue.length, - EVP_sha1(), NULL); + key->keyvalue.data, key->keyvalue.length, + EVP_sha1(), NULL); if (sizeof(digest) != HMAC_size(&ctx)) - krb5_abortx(context, "runtime error, hmac buffer wrong size in kx509"); + 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); @@ -160,9 +187,9 @@ verify_req_hash(krb5_context context, 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; + krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED, + "kx509 request MAC mismatch"); + return KRB5KDC_ERR_PREAUTH_FAILED; } return 0; } @@ -203,8 +230,8 @@ kx509_log(krb5_context context, */ static krb5_error_code calculate_reply_hash(krb5_context context, - krb5_keyblock *key, - Kx509Response *rep) + krb5_keyblock *key, + Kx509Response *rep) { krb5_error_code ret; HMAC_CTX ctx; @@ -212,17 +239,17 @@ calculate_reply_hash(krb5_context context, HMAC_CTX_init(&ctx); HMAC_Init_ex(&ctx, key->keyvalue.data, key->keyvalue.length, - EVP_sha1(), NULL); + EVP_sha1(), NULL); ret = krb5_data_alloc(rep->hash, HMAC_size(&ctx)); if (ret) { - HMAC_CTX_cleanup(&ctx); - krb5_set_error_message(context, ENOMEM, "malloc: out of memory"); - return ENOMEM; + HMAC_CTX_cleanup(&ctx); + krb5_set_error_message(context, ENOMEM, "malloc: out of memory"); + return ENOMEM; } HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); { - int32_t t = rep->error_code; + int32_t t = rep->error_code; unsigned char encint[sizeof(t) + 1]; size_t k; @@ -244,9 +271,9 @@ calculate_reply_hash(krb5_context context, HMAC_Update(&ctx, &encint[sizeof(encint)] - k, k); } if (rep->certificate) - HMAC_Update(&ctx, rep->certificate->data, rep->certificate->length); + 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_Update(&ctx, (unsigned char *)*rep->e_text, strlen(*rep->e_text)); HMAC_Final(&ctx, rep->hash->data, 0); HMAC_CTX_cleanup(&ctx); @@ -254,438 +281,11 @@ calculate_reply_hash(krb5_context context, return 0; } -/* - * Lookup the principal's HDB entry, authorize the requested extensions, add - * authorized extensions to the `tbs', and indicate whether to add any of the - * EKUs/SANs we'd normally add automatically. - */ -static krb5_error_code -get_hdb_ekus_and_sans(krb5_context context, - kx509_req_context reqctx, - krb5_principal principal, - hx509_ca_tbs tbs, - int *add_auto_exts) +static void +frees(char **s) { - *add_auto_exts = 1; - return ENOTSUP; -} - -/* - * Finds a template in the configuration that is appropriate to the form of the - * client principal. Also sets some variables in `env' and adds some SANs to - * `tbs'` as appropriate (others are added in build_certificate()). - * - * TODO: - * - support templates as strings in configuration? - * - lookup a 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) - * - add code to build a template on the fly - */ -static krb5_error_code -get_template(krb5_context context, - kx509_req_context reqctx, - krb5_principal principal, - const char *princ_no_realm, - const char *princ, - const char **template, - hx509_env *env, - hx509_ca_tbs tbs) -{ - krb5_error_code ret = KRB5KDC_ERR_POLICY; - unsigned int ncomp = krb5_principal_get_num_comp(context, principal); - const char *crealm = krb5_principal_get_realm(context, principal); - const char *kx509_template = NULL; - const char *comp0, *comp1, *comp2; - char *domain = NULL; - char *email = NULL; - int add_auto_exts = 1; - - /* Populate extensions from CSR / HDB entry as requested and permitted */ - ret = get_hdb_ekus_and_sans(context, reqctx, principal, tbs, - &add_auto_exts); - if (ret != 0 && ret != ENOTSUP) - return ret; - - if (ncomp == 1) { - /* 1-component, user principal */ - - /* Find the template */ - kx509_template = krb5_config_get_string(context, NULL, "kdc", "realms", - crealm, "kx509_template", - NULL); - if (kx509_template == NULL) - kx509_template = reqctx->config->kx509_template; - if (kx509_template == NULL) - goto out; - - /* - * Add [some of] the "env" variables we support for 1-component - * pincipals. Others are added in build_certificate(). - */ - ret = hx509_env_add(context->hx509ctx, env, "principal-component0", - princ_no_realm); - - /* - * If configured, include an rfc822Name that's just the client's - * principal name @ down-cased realm. - * - * XXX Dicey feature! Maybe this should be a string param whose value - * is the domainname to use for the email address. - */ - if (ret == 0 && add_auto_exts && - get_bool_param(context, FALSE, crealm, "kx509_include_email_san")) { - char *p; - - if ((domain = strdup(crealm)) == NULL) { - ret = ENOMEM; - goto out; - } - for (p = domain; *p; p++) - *p = isupper(*p) ? tolower(*p) : *p; - if (asprintf(&email, "%s@%s", princ_no_realm, domain) == -1 || - email == NULL) { - ret = ENOMEM; - goto out; - } - ret = hx509_ca_tbs_add_san_rfc822name(context->hx509ctx, tbs, email); - } - } else if (ncomp == 2 || ncomp == 3) { - const char *config_label; - - /* - * 2- and 3-component principal name. - * - * We do not have a reliable name-type indicator. If the second - * component has a '.' in it then we'll assume that the name is a - * host-based (2-component) or domain-based (3-component) service - * principal name. Else we'll assume it's a two-component admin-style - * username. - */ - - comp0 = krb5_principal_get_comp_string(context, principal, 0); - comp1 = krb5_principal_get_comp_string(context, principal, 1); - comp2 = ncomp == 3 ? - krb5_principal_get_comp_string(context, principal, 2) : NULL; - - ret = hx509_env_add(context->hx509ctx, env, "principal-component0", - comp0); - if (ret == 0) - ret = hx509_env_add(context->hx509ctx, env, "principal-component1", - comp1); - if (ret == 0 && ncomp == 3) - ret = hx509_env_add(context->hx509ctx, env, "principal-component2", - comp2); - - if (ret == 0 && strchr(comp1, '.')) { - /* Looks like host-based or domain-based service */ - config_label = ncomp == 2 ? "hostbased" : "domainbased"; - - ret = hx509_env_add(context->hx509ctx, env, - "principal-service-name", comp0); - if (ret == 0) - ret = hx509_env_add(context->hx509ctx, env, "principal-host-name", comp1); - if (ret == 0 && ncomp == 3) - ret = hx509_env_add(context->hx509ctx, env, "principal-domain-name", comp2); - - if (ret == 0 && add_auto_exts && - get_bool_param(context, FALSE, crealm, - "kx509_include_dnsname_san")) { - ret = hx509_ca_tbs_add_san_hostname(context->hx509ctx, tbs, comp1); - } - } else if (ret == 0 && ncomp == 2) { - /* Looks like a kadmin/username or similar name */ - config_label = "two_component_user"; - } else { - if (ret == 0) - ret = KRB5KDC_ERR_POLICY; - goto out; - } - - /* Find the template */ - kx509_template = krb5_config_get_string(context, NULL, "kdc", "realms", - crealm, "kx509_templates", - config_label, comp0, NULL); - if (kx509_template == NULL) - kx509_template = krb5_config_get_string(context, NULL, "kdc", - "kx509_templates", - config_label, comp0, NULL); - if (kx509_template == NULL) { - kdc_log(context, reqctx->config, 2, "kx509 template not found for %s", - princ); - ret = KRB5KDC_ERR_POLICY; - goto out; - } - } else { - kdc_log(context, reqctx->config, 2, "kx509 client %s has too many components!", - princ); - ret = KRB5KDC_ERR_POLICY; - } - -out: - *template = kx509_template; - free(domain); - free(email); - 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_cert cert, - const char *chain_store, - krb5_data *out) -{ - krb5_error_code ret; - Certificates cs; - hx509_certs certs = NULL; - size_t len; - - cs.len = 0; - cs.val = 0; - - ret = chain_add1_func(hx509ctx, &cs, cert); - if (ret == 0) - ret = hx509_certs_init(hx509ctx, chain_store, 0, NULL, &certs); - if (ret == 0) - ret = hx509_certs_iter_f(hx509ctx, certs, chain_add1_func, &cs); - hx509_certs_free(&certs); - if (ret == 0) - ASN1_MALLOC_ENCODE(Certificates, out->data, out->length, - &cs, &len, ret); - free_Certificates(&cs); - return ret; -} - - -/* - * Build a certifate for `principal´ that will expire at `endtime´. - */ -static krb5_error_code -build_certificate(krb5_context context, - kx509_req_context reqctx, - time_t endtime, - krb5_principal principal, - krb5_data *certificate) -{ - const char *crealm = krb5_principal_get_realm(context, principal); - const char *kx509_ca; - const char *kx509_template; - char *princ = NULL; - char *name = NULL; - hx509_ca_tbs tbs = NULL; - hx509_env env = NULL; - hx509_cert cert = NULL; - hx509_cert signer = NULL; - krb5_error_code ret; - - ret = hx509_ca_tbs_init(context->hx509ctx, &tbs); - if (ret) - goto out; - - /* Pick an issuer based on the crealm if we can */ - kx509_ca = krb5_config_get_string(context, NULL, "kdc", "realms", crealm, - "kx509_ca", NULL); - if (kx509_ca == NULL) - kx509_ca = reqctx->config->kx509_ca; - if (kx509_ca == NULL) { - ret = KRB5KDC_ERR_POLICY; - kdc_log(context, reqctx->config, 0, - "No kx509 CA credential specified for realm %s", crealm); - goto out; - } - - ret = krb5_unparse_name_flags(context, principal, - KRB5_PRINCIPAL_UNPARSE_NO_REALM, - &name); - if (ret) - goto out; - - ret = krb5_unparse_name(context, principal, &princ); - if (ret) - goto out; - - /* Get a template and set things in `env' and `tbs' as appropriate */ - ret = get_template(context, reqctx, principal, name, princ, - &kx509_template, &env, tbs); - if (ret) - goto out; - if (kx509_template == NULL) { - kdc_log(context, reqctx->config, 0, - "No kx509 certificate template specified"); - ret = KRB5KDC_ERR_POLICY; - goto out; - } - - kdc_log(context, reqctx->config, 3, "Issuing kx509 certificate to %s " - "using template %s", princ, kx509_template); - - /* - * Populate additional template "env" variables - */ - ret = hx509_env_add(context->hx509ctx, &env, - "principal-name-without-realm", name); - if (ret == 0) - ret = hx509_env_add(context->hx509ctx, &env, "principal-name", princ); - if (ret == 0) - ret = hx509_env_add(context->hx509ctx, &env, "principal-name-realm", - crealm); - if (ret) - goto out; - - /* Load the issuer certificate and private key */ - { - hx509_certs certs; - hx509_query *q; - - ret = hx509_certs_init(context->hx509ctx, kx509_ca, 0, NULL, &certs); - if (ret) { - kdc_log(context, reqctx->config, 0, - "Failed to load CA %s", kx509_ca); - goto out; - } - ret = hx509_query_alloc(context->hx509ctx, &q); - if (ret) { - hx509_certs_free(&certs); - goto out; - } - - hx509_query_match_option(q, HX509_QUERY_OPTION_PRIVATE_KEY); - hx509_query_match_option(q, HX509_QUERY_OPTION_KU_KEYCERTSIGN); - - ret = hx509_certs_find(context->hx509ctx, certs, q, &signer); - hx509_query_free(context->hx509ctx, q); - hx509_certs_free(&certs); - if (ret) { - kdc_log(context, reqctx->config, 0, - "Failed to find a CA in %s", kx509_ca); - goto out; - } - } - - /* Populate the subject public key in the TBS context */ - { - SubjectPublicKeyInfo spki; - - ret = hx509_request_get_SubjectPublicKeyInfo(context->hx509ctx, - reqctx->csr, - &spki); - if (ret == 0) - ret = hx509_ca_tbs_set_spki(context->hx509ctx, tbs, &spki); - free_SubjectPublicKeyInfo(&spki); - if (ret) - goto out; - } - - /* Load the template into the TBS context */ - { - hx509_certs certs; - hx509_cert template; - - ret = hx509_certs_init(context->hx509ctx, kx509_template, 0, - NULL, &certs); - if (ret == 0) - ret = hx509_get_one_cert(context->hx509ctx, certs, &template); - hx509_certs_free(&certs); - if (ret) { - kdc_log(context, reqctx->config, 0, - "Failed to load template from %s", kx509_template); - goto out; - } - - /* - * Only take the subjectName, the keyUsage, and EKUs from the template - * certificate. - */ - ret = hx509_ca_tbs_set_template(context->hx509ctx, tbs, - HX509_CA_TEMPLATE_SUBJECT | - HX509_CA_TEMPLATE_KU | - HX509_CA_TEMPLATE_EKU, - template); - hx509_cert_free(template); - if (ret) - goto out; - } - - /* - * Add other SANs. - * - * Adding an id-pkinit-san means the client can use the certificate to - * initiate PKINIT. That might seem odd, but it enables a sort of PKIX - * credential delegation by allowing forwarded Kerberos tickets to be - * used to acquire PKIX credentials. Thus this can work: - * - * PKIX (w/ HW token) -> Kerberos -> - * PKIX (w/ softtoken) -> Kerberos -> - * PKIX (w/ softtoken) -> Kerberos -> - * ... - * - * Note that we may not have added the PKINIT EKU -- that depends on the - * template, and host-based service templates might well not include it. - */ - if (ret == 0 && - get_bool_param(context, TRUE, crealm, "kx509_include_pkinit_san")) { - ret = hx509_ca_tbs_add_san_pkinit(context->hx509ctx, tbs, princ); - if (ret) - goto out; - } - - /* - * Note that we set the certificate's end time to the client's *Ticket*'s - * end time. For server certs this may not always be appropriate. We - * might want to have a configurable setting for this, in which case maybe - * we should move this to get_template(). - */ - hx509_ca_tbs_set_notAfter(context->hx509ctx, tbs, endtime); - - /* Expand the subjectName template in the TBS */ - hx509_ca_tbs_subject_expand(context->hx509ctx, tbs, env); - hx509_env_free(&env); - - /* All done with the TBS, sign/issue the certificate */ - ret = hx509_ca_sign(context->hx509ctx, tbs, signer, &cert); - if (ret) - goto out; - - /* Encode and output the certificate */ - if (reqctx->send_chain) - ret = encode_cert_and_chain(context->hx509ctx, cert, kx509_ca, certificate); - else - ret = hx509_cert_binary(context->hx509ctx, cert, certificate); - -out: - krb5_xfree(name); - krb5_xfree(princ); - if (env) - hx509_env_free(&env); - if (tbs) - hx509_ca_tbs_free(&tbs); - if (cert) - hx509_cert_free(cert); - if (signer) - hx509_cert_free(signer); - return ret; + free(*s); + *s = NULL; } /* Check that a krbtgt's second component is a local realm */ @@ -731,8 +331,8 @@ is_local_realm(krb5_context context, */ static krb5_error_code kdc_kx509_verify_service_principal(krb5_context context, - kx509_req_context reqctx, - krb5_principal sprincipal) + kx509_req_context reqctx, + krb5_principal sprincipal) { krb5_error_code ret = 0; krb5_principal principal = NULL; @@ -754,25 +354,25 @@ kdc_kx509_verify_service_principal(krb5_context context, /* Must be hostbased kca_service name then */ ret = gethostname(localhost, sizeof(localhost) - 1); if (ret != 0) { - ret = errno; - krb5_set_error_message(context, ret, - N_("Failed to get local hostname", "")); - return ret; + ret = errno; + krb5_set_error_message(context, ret, + N_("Failed to get local hostname", "")); + return ret; } localhost[sizeof(localhost) - 1] = '\0'; ret = krb5_make_principal(context, &principal, "", "kca_service", - localhost, NULL); + localhost, NULL); if (ret) - goto out; + goto out; if (krb5_principal_compare_any_realm(context, sprincipal, principal)) - goto out; /* found a match */ + goto out; /* found a match */ err: ret = krb5_unparse_name(context, sprincipal, &expected); if (ret) - goto out; + goto out; ret = KRB5KDC_ERR_SERVER_NOMATCH; kx509_log(context, reqctx, 2, "client used wrong kx509 service principal " @@ -962,9 +562,10 @@ update_csr(krb5_context context, kx509_req_context reqctx, Extensions *exts) ret = decode_GeneralNames(e->extnValue.data, e->extnValue.length, &san, NULL); - for (k = 0; ret == 0 && k < san.len; k++) + for (k = 0; ret == 0 && k < san.len; k++) { ret = hx509_request_add_GeneralName(context->hx509ctx, reqctx->csr, &san.val[k]); + } free_GeneralNames(&san); } } @@ -990,9 +591,8 @@ get_csr(krb5_context context, kx509_req_context reqctx) ret = decode_Kx509CSRPlus(pk_key.data, pk_key.length, &reqctx->csr_plus, &size); if (ret == 0) { + reqctx->have_csr = 1; reqctx->send_chain = 1; - if (reqctx->csr_plus.authz_datas.len) - reqctx->have_auth_data = 1; /* Parse CSR */ ret = hx509_request_parse_der(context->hx509ctx, &reqctx->csr_plus.csr, @@ -1009,6 +609,7 @@ get_csr(krb5_context context, kx509_req_context reqctx) 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")) @@ -1030,14 +631,193 @@ get_csr(krb5_context context, kx509_req_context reqctx) "Could not decode CSR or RSA subject public key"); } -/* Stub for later work */ -static krb5_error_code -verify_auth_data(krb5_context context, - struct kx509_req_context *reqctx, - krb5_principal cprincipal, - krb5_principal *actual_cprincipal) +/* + * 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) { - return EACCES; + 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); + 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, reqctx->csr, cprincipal); + if (ret == 0) { + kx509_log(context, reqctx, 0, "Requested extensions authorized " + "by plugin"); + return 0; + } + if (ret != KRB5_PLUGIN_NO_HANDLE) { + kx509_log(context, reqctx, 0, "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++) { + hx509_san_type san_type; + + 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)) { + kx509_log(context, reqctx, 0, "Requested extensions rejected " + "by default policy (dNSName SAN %s does not match " + "client %s)", s, cprinc); + goto eacces; + } + break; + case HX509_SAN_TYPE_PKINIT: + if (strcmp(cprinc, s) != 0) { + kx509_log(context, reqctx, 0, "Requested extensions rejected " + "by default policy (PKINIT SAN %s does not match " + "client %s)", s, cprinc); + goto eacces; + } + break; + default: + ret = ENOTSUP; + } + } + 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))) { + free(cprinc); + free(s); + 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])) + 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; + + return 0; + +eacces: + ret = EACCES; +out: + 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; } /* @@ -1046,23 +826,22 @@ verify_auth_data(krb5_context context, krb5_error_code _kdc_do_kx509(krb5_context context, - krb5_kdc_configuration *config, - const struct Kx509Request *req, krb5_data *reply, - const char *from, struct sockaddr *addr) + krb5_kdc_configuration *config, + const struct Kx509Request *req, krb5_data *reply, + const char *from, struct sockaddr *addr) { - krb5_error_code ret; + krb5_error_code ret = 0; krb5_ticket *ticket = NULL; krb5_flags ap_req_options; - krb5_principal actual_cprincipal = NULL; krb5_principal cprincipal = NULL; krb5_principal sprincipal = NULL; krb5_keytab id = NULL; Kx509Response rep; + hx509_certs certs = NULL; struct kx509_req_context reqctx; int is_probe = 0; memset(&reqctx, 0, sizeof(reqctx)); - reqctx.csr_plus.authz_datas.val = NULL; reqctx.csr_plus.csr.data = NULL; reqctx.csr_plus.exts = NULL; reqctx.config = config; @@ -1085,7 +864,6 @@ _kdc_do_kx509(krb5_context context, krb5_data_zero(reply); memset(&rep, 0, sizeof(rep)); - if (req->authenticator.length == 0) { /* * Unauthenticated kx509 service availability probe. @@ -1100,37 +878,32 @@ _kdc_do_kx509(krb5_context context, goto out; } - /* Consume the AP-REQ */ + /* Authenticate the request (consume the AP-REQ) */ ret = krb5_kt_resolve(context, "HDBGET:", &id); if (ret) { ret = mk_error_response(context, &reqctx, KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, "Can't open HDB/keytab for kx509"); - goto out; + goto out; } ret = krb5_rd_req(context, - &reqctx.ac, - &req->authenticator, - NULL, - id, - &ap_req_options, - &ticket); + &reqctx.ac, + &req->authenticator, + NULL, + id, + &ap_req_options, + &ticket); if (ret == 0) ret = krb5_auth_con_getkey(context, reqctx.ac, &reqctx.key); if (ret == 0 && reqctx.key == NULL) - ret = KRB5KDC_ERR_NULL_KEY; + 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(context, ticket, &cprincipal); - if (ret) { - ret = mk_error_response(context, &reqctx, ret, - "authentication failed"); - goto out; - } /* Optional: check if Ticket is INITIAL */ if (ret == 0 && @@ -1138,7 +911,7 @@ _kdc_do_kx509(krb5_context context, !get_bool_param(context, TRUE, krb5_principal_get_realm(context, cprincipal), "require_initial_kca_tickets")) { - ret = mk_error_response(context, &reqctx, KRB5KDC_ERR_POLICY, /* XXX */ + ret = mk_error_response(context, &reqctx, KRB5KDC_ERR_POLICY, "client used non-INITIAL tickets, but kx509" "kx509 service is configured to require " "INITIAL tickets"); @@ -1159,19 +932,19 @@ _kdc_do_kx509(krb5_context context, if (ret) { mk_error_response(context, &reqctx, ret, "client used incorrect service name"); - goto out; + goto out; } /* Authenticate the rest of the request */ ret = verify_req_hash(context, req, reqctx.key); if (ret) { mk_error_response(context, &reqctx, ret, "Incorrect request HMAC"); - goto out; + goto out; } if (req->pk_key.length == 0) { /* - * The request is a kx509 service availability probe. + * 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. @@ -1179,7 +952,7 @@ _kdc_do_kx509(krb5_context context, is_probe = 1; ret = mk_error_response(context, &reqctx, 0, "kx509 authenticated probe request"); - goto out; + goto out; } /* Extract and parse CSR or a DER-encoded RSA public key */ @@ -1187,16 +960,14 @@ _kdc_do_kx509(krb5_context context, if (ret) goto out; - if (reqctx.have_auth_data) { - ret = verify_auth_data(context, &reqctx, cprincipal, - &actual_cprincipal); - if (ret) { - ret = mk_error_response(context, &reqctx, ret, - "authorization data validation failure"); - goto out; - } + /* Authorize the request */ + ret = check_authz(context, &reqctx, cprincipal); + if (ret) { + ret = mk_error_response(context, &reqctx, ret, "rejected by policy"); + goto out; } + /* Issue the certificate */ ALLOC(rep.hash); ALLOC(rep.certificate); if (rep.certificate == NULL || rep.hash == NULL) { @@ -1205,15 +976,21 @@ _kdc_do_kx509(krb5_context context, goto out; } - /* Issue the certificate */ krb5_data_zero(rep.hash); krb5_data_zero(rep.certificate); - ret = build_certificate(context, &reqctx, - krb5_ticket_get_endtime(context, ticket), - actual_cprincipal ? actual_cprincipal : cprincipal, - rep.certificate); + krb5_ticket_get_times(context, ticket, &reqctx.ticket_times); + ret = kdc_issue_certificate(context, reqctx.config, reqctx.csr, cprincipal, + &reqctx.ticket_times, reqctx.send_chain, + &certs); if (ret) { - mk_error_response(context, &reqctx, ret, "Failed to build certificate"); + mk_error_response(context, &reqctx, ret, "Certificate isuance failed"); + goto out; + } + + ret = encode_cert_and_chain(context->hx509ctx, certs, rep.certificate); + if (ret) { + mk_error_response(context, &reqctx, ret, + "Could not encode certificate and chain"); goto out; } @@ -1222,7 +999,7 @@ _kdc_do_kx509(krb5_context context, if (ret) { mk_error_response(context, &reqctx, ret, "Failed to compute response HMAC"); - goto out; + goto out; } /* Encode and output reply */ @@ -1232,28 +1009,27 @@ _kdc_do_kx509(krb5_context context, kx509_log(context, &reqctx, 1, "Could not encode response"); out: + hx509_certs_free(&certs); if (ret == 0 && !is_probe) kx509_log(context, &reqctx, 3, "Issued certificate"); else kx509_log(context, &reqctx, 2, "Did not issue certificate"); if (reqctx.ac) - krb5_auth_con_free(context, reqctx.ac); + krb5_auth_con_free(context, reqctx.ac); if (ticket) - krb5_free_ticket(context, ticket); + krb5_free_ticket(context, ticket); if (id) - krb5_kt_close(context, id); + krb5_kt_close(context, id); if (sprincipal) - krb5_free_principal(context, sprincipal); + krb5_free_principal(context, sprincipal); if (cprincipal) - krb5_free_principal(context, cprincipal); - if (actual_cprincipal) - krb5_free_principal(context, actual_cprincipal); + krb5_free_principal(context, cprincipal); if (reqctx.key) - krb5_free_keyblock (context, reqctx.key); + krb5_free_keyblock (context, reqctx.key); if (reqctx.sname) - free(reqctx.sname); + free(reqctx.sname); if (reqctx.cname) - free(reqctx.cname); + free(reqctx.cname); hx509_request_free(&reqctx.csr); free_Kx509CSRPlus(&reqctx.csr_plus); free_Kx509Response(&rep); diff --git a/kdc/libkdc-exports.def b/kdc/libkdc-exports.def index 65250537a..28d4aeb90 100644 --- a/kdc/libkdc-exports.def +++ b/kdc/libkdc-exports.def @@ -1,9 +1,12 @@ EXPORTS + kdc_authorize_csr kdc_get_instance + kdc_issue_certificate kdc_log kdc_log_msg kdc_log_msg_va kdc_openlog + kdc_validate_token krb5_kdc_windc_init krb5_kdc_get_config krb5_kdc_pkinit_config diff --git a/kdc/negotiate_token_validator.c b/kdc/negotiate_token_validator.c new file mode 100644 index 000000000..64294b746 --- /dev/null +++ b/kdc/negotiate_token_validator.c @@ -0,0 +1,322 @@ +/* + * 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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); + /* XXX gss_get_instance() doesn't exist :( */ + return 0; +} + +krb5_plugin_load_ft kdc_token_validator_plugin_load; + +krb5_error_code KRB5_CALLCONV +kdc_token_validator_plugin_load(krb5_context 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/kdc/simple_csr_authorizer.c b/kdc/simple_csr_authorizer.c new file mode 100644 index 000000000..b723a795a --- /dev/null +++ b/kdc/simple_csr_authorizer.c @@ -0,0 +1,337 @@ +/* + * 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 checking for + * existence of files of the form: + * + * + * ///- + * + * where is the value of: + * + * [kdc] simple_csr_authorizer_directory = PATH + * + * is a requesting client principal name with all characters other than + * alphanumeric, '-', '_', and non-leading '.' URL-encoded. + * + * is one of: + * + * - pkinit (SAN) + * - xmpt (SAN) + * - emailt (SAN) + * - ms-upt (SAN) + * - dnsnamt (SAN) + * - eku (EKU OID) + * + * and 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. + */ +#define _GNU_SOURCE 1 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/* + * 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(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 KRB5_LIB_CALL krb5_error_code +authorize(void *ctx, + krb5_context context, + krb5_kdc_configuration *config, + hx509_request csr, + krb5_const_principal client, + krb5_boolean *result) +{ + krb5_error_code ret; + hx509_context hx509ctx = NULL; + KeyUsage ku; + const char *d; + size_t i; + char *princ = NULL; + char *s = NULL; + + if ((d = krb5_config_get_string(context, NULL, "kdc", + "simple_csr_authorizer_directory", + NULL)) == NULL) + return KRB5_PLUGIN_NO_HANDLE; + + if ((ret = hx509_context_init(&hx509ctx))) + return ret; + + if ((ret = krb5_unparse_name(context, client, &princ))) + goto out; + + s = string_encode(princ); + free(princ); + princ = NULL; + if (s == NULL) + goto enomem; + + princ = s; + s = NULL; + + for (i = 0; ret == 0; i++) { + hx509_san_type san_type; + struct stat st; + const char *prefix; + char *san; + char *p; + + ret = hx509_request_get_san(csr, i, &san_type, &s); + if (ret) + break; + switch (san_type) { + case HX509_SAN_TYPE_EMAIL: + prefix = "email"; + break; + case HX509_SAN_TYPE_DNSNAME: + prefix = "dnsname"; + break; + case HX509_SAN_TYPE_XMPP: + prefix = "xmpp"; + break; + case HX509_SAN_TYPE_PKINIT: + prefix = "pkinit"; + break; + case HX509_SAN_TYPE_MS_UPN: + prefix = "ms-upn"; + break; + default: + ret = ENOTSUP; + break; + } + if (ret) + break; + + if ((san = string_encode(s)) == NULL || + asprintf(&p, "%s/%s/%s-%s", d, princ, prefix, san) == -1 || + p == NULL) + goto enomem; + ret = stat(p, &st) == -1 ? errno : 0; + free(san); + free(p); + free(s); + s = NULL; + if (ret) + goto skip; + ret = hx509_request_authorize_san(csr, i); + } + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret) + goto out; + + for (i = 0; ret == 0; i++) { + struct stat st; + char *p; + + ret = hx509_request_get_eku(csr, i, &s); + if (ret) + break; + if (asprintf(&p, "%s/%s/eku-%s", d, princ, s) == -1 || p == NULL) { + free(princ); + free(s); + } + ret = stat(p, &st) == -1 ? errno : 0; + free(p); + free(s); + s = NULL; + if (ret) + goto skip; + ret = hx509_request_authorize_eku(csr, i); + } + 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); + + *result = TRUE; + ret = 0; + goto out; + +skip: + /* Allow another plugin to get a crack at this */ + ret = KRB5_PLUGIN_NO_HANDLE; + goto out; + +enomem: + ret = krb5_enomem(context); + goto out; + +out: + hx509_context_free(&hx509ctx); + free(princ); + free(s); + return ret; +} + +static KRB5_LIB_CALL krb5_error_code +simple_csr_authorizer_init(krb5_context context, void **c) +{ + *c = NULL; + return 0; +} + +static KRB5_LIB_CALL void +simple_csr_authorizer_fini(void *c) +{ +} + +static krb5plugin_csr_authorizer_ftable plug_desc = + { 1, simple_csr_authorizer_init, simple_csr_authorizer_fini, authorize }; + +static krb5plugin_csr_authorizer_ftable *plugs[] = { &plug_desc }; + +static uintptr_t +simple_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(krb5_context context, + krb5_get_instance_func_t *get_instance, + size_t *num_plugins, + krb5_plugin_common_ftable_cp **plugins) +{ + *get_instance = simple_csr_authorizer_get_instance; + *num_plugins = sizeof(plugs) / sizeof(plugs[0]); + *plugins = (krb5_plugin_common_ftable_cp *)plugs; + return 0; +} diff --git a/kdc/test_csr_authorizer.c b/kdc/test_csr_authorizer.c new file mode 100644 index 000000000..b9cbf924f --- /dev/null +++ b/kdc/test_csr_authorizer.c @@ -0,0 +1,79 @@ +#include "kdc_locl.h" + +static int help_flag; +static int version_flag; +static const char *app_string = "kdc"; + +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" }, +}; +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, + "\n\tExercise CSR authorization plugins for a given CSR for a\n" + "\tgiven principal.\n" + "\n\tExample: %s PKCS10:/tmp/csr.der foo@TEST.H5L.SE\n", + getprogname()); + exit(e); + return e; +} + +int +main(int argc, char **argv) +{ + krb5_kdc_configuration *config; + 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; + } + + argc -= optidx; + argv += optidx; + + if (argc != 2) + usage(1); + + if ((errno = krb5_init_context(&context))) + err(1, "Could not initialize krb5_context"); + if ((ret = krb5_kdc_get_config(context, &config))) + krb5_err(context, 1, ret, "Could not get KDC configuration"); + config->app = app_string; + if ((ret = krb5_initlog(context, argv0, &config->logf)) || + (ret = krb5_addlog_dest(context, config->logf, "0-5/STDERR"))) + krb5_err(context, 1, ret, "Could not set up logging to stderr"); + if ((ret = krb5_kdc_set_dbinfo(context, config))) + krb5_err(context, 1, ret, "Could not get KDC configuration (HDB)"); + 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]); + if ((ret = krb5_parse_name(context, argv[1], &princ))) + krb5_err(context, 1, ret, "Could not parse principal %s", argv[1]); + if ((ret = kdc_authorize_csr(context, config, csr, princ))) + krb5_err(context, 1, ret, "Authorization failed"); + printf("Authorized!\n"); + krb5_free_principal(context, princ); + krb5_free_context(context); + hx509_request_free(&csr); + /* FIXME There's no free function for config yet */ + return 0; +} diff --git a/kdc/test_kdc_ca.c b/kdc/test_kdc_ca.c new file mode 100644 index 000000000..18e92a212 --- /dev/null +++ b/kdc/test_kdc_ca.c @@ -0,0 +1,146 @@ +#include "kdc_locl.h" + +static int authorized_flag; +static int help_flag; +static const char *app_string = "kdc"; +static int version_flag; + +struct getargs args[] = { + { "authorized", 'A', arg_flag, &authorized_flag, + "Assume CSR is authorized", NULL }, + { "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; +} + +int +main(int argc, char **argv) +{ + krb5_kdc_configuration *config; + 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"; + 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_kdc_get_config(context, &config))) + krb5_err(context, 1, ret, "Could not get KDC configuration"); + if ((ret = krb5_initlog(context, argv0, &config->logf)) || + (ret = krb5_addlog_dest(context, config->logf, "0-5/STDERR"))) + krb5_err(context, 1, ret, "Could not set up logging to stderr"); +#if 0 + if ((ret = krb5_kdc_set_dbinfo(context, config))) + krb5_err(context, 1, ret, "Could not get KDC configuration (HDB)"); +#endif + 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 == HX509_NO_ITEM) + ret = 0; + } else if ((ret = kdc_authorize_csr(context, config, 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; + if ((ret = kdc_issue_certificate(context, config, req, p, &t, 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_free_principal(context, p); + krb5_free_context(context); + hx509_request_free(&req); + hx509_certs_free(&store); + hx509_certs_free(&certs); + /* FIXME There's no free function for config yet */ + return 0; +} diff --git a/kdc/test_token_validator.c b/kdc/test_token_validator.c new file mode 100644 index 000000000..88b1cc47a --- /dev/null +++ b/kdc/test_token_validator.c @@ -0,0 +1,86 @@ +#include "kdc_locl.h" + +static int help_flag; +static int version_flag; +static char *realm; +static struct getarg_strings audiences; + +struct getargs args[] = { + { "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; +} + +int +main(int argc, char **argv) +{ + krb5_kdc_configuration *config; + 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 ((ret = krb5_init_context(&context))) + err(1, "Could not initialize krb5_context"); + if ((ret = krb5_kdc_get_config(context, &config))) + krb5_err(context, 1, ret, "Could not get KDC configuration"); + + 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_free_principal(context, actual_princ); + return 0; +} diff --git a/kdc/token_validator.c b/kdc/token_validator.c new file mode 100644 index 000000000..d3ee8752a --- /dev/null +++ b/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 krb5_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. + */ +krb5_error_code +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/kdc/token_validator_plugin.h b/kdc/token_validator_plugin.h new file mode 100644 index 000000000..73b05e5d0 --- /dev/null +++ b/kdc/token_validator_plugin.h @@ -0,0 +1,85 @@ +/* + * 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 { + int minor_version; + krb5_error_code (KRB5_LIB_CALL *init)(krb5_context, void **); + void (KRB5_LIB_CALL *fini)(void *); + 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/kdc/version-script.map b/kdc/version-script.map index 4f5be7a28..ba7b0fe93 100644 --- a/kdc/version-script.map +++ b/kdc/version-script.map @@ -2,12 +2,15 @@ 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_windc_init; krb5_kdc_get_config; krb5_kdc_pkinit_config; diff --git a/kuser/kx509.c b/kuser/kx509.c index 8ae284138..a31889e5b 100644 --- a/kuser/kx509.c +++ b/kuser/kx509.c @@ -287,7 +287,8 @@ kx509(struct kx509_options *opt, int argc, char **argv) ret = krb5_kx509_ctx_set_key(context, req, opt->private_key_string); if (ret) - krb5_err(context, 1, ret, "could not setup kx509 request options"); + krb5_err(context, 1, ret, + "could not set up kx509 request options"); ret = krb5_kx509_ext(context, req, cc, opt->out_string, ccout); if (ret) diff --git a/lib/asn1/krb5.asn1 b/lib/asn1/krb5.asn1 index 560109997..5031e3921 100644 --- a/lib/asn1/krb5.asn1 +++ b/lib/asn1/krb5.asn1 @@ -216,7 +216,12 @@ AUTHDATA-TYPE ::= INTEGER { -- N.B. these assignments have not been confirmed yet. -- -- DO NOT USE in production yet! - KRB5-AUTHDATA-ON-BEHALF-OF(580) -- UTF8String princ name + KRB5-AUTHDATA-ON-BEHALF-OF(580), -- UTF8String princ name + KRB5-AUTHDATA-BEARER-TOKEN-JWT(581), -- JWT token + KRB5-AUTHDATA-BEARER-TOKEN-SAML(582), -- SAML token + KRB5-AUTHDATA-BEARER-TOKEN-OIDC(583), -- OIDC token + KRB5-AUTHDATA-CSR-AUTHORIZED(584) -- Proxy has authorized client + -- to requested exts in CSR } -- checksumtypes diff --git a/lib/asn1/kx509.asn1 b/lib/asn1/kx509.asn1 index 9c7b43ae0..64c9b9c5f 100644 --- a/lib/asn1/kx509.asn1 +++ b/lib/asn1/kx509.asn1 @@ -7,7 +7,8 @@ KX509 DEFINITIONS ::= BEGIN IMPORTS Extensions FROM rfc2459 - KerberosTime, AUTHDATA-TYPE FROM krb5; + KerberosTime FROM krb5 + KRB5PrincipalName FROM pkinit; KX509-ERROR-CODE ::= INTEGER { KX509-STATUS-GOOD(0), @@ -61,12 +62,6 @@ KX509-ERROR-CODE ::= INTEGER { Kx509CSRPlus ::= [APPLICATION 35] SEQUENCE { -- PKCS#10, DER-encoded CSR, with or without meaningful attributes csr OCTET STRING, - -- The AP-REQ's Authenticator may contain authz-data of interest here - -- for carrying confidential payloads. E.g., a bearer token for a user - -- to impersonate. This sequence tells the server what authz-data - -- elements there might be, effectively making them critical even if - -- they are in AD-IF-RELEVANT containers. - authz-datas SEQUENCE OF AUTHDATA-TYPE, -- Desired certificate Extensions such as KeyUsage, ExtKeyUsage, or -- subjectAlternativeName (SAN) exts Extensions OPTIONAL, diff --git a/lib/krb5/krb5.conf.5 b/lib/krb5/krb5.conf.5 index 5d4cc369d..c4c986b50 100644 --- a/lib/krb5/krb5.conf.5 +++ b/lib/krb5/krb5.conf.5 @@ -753,79 +753,106 @@ Specifies the digests the kdc will reply to. The default is .Li ntlm-v2 . .It Li enable-kx509 = Va boolean Enables kx509 service. -.It Li kx509_ca = Va file -Specifies the PEM credentials for the kx509 certification authority. +.Pp +The kx509 service is configurable for a number of cases: +.Bl -tag -width "" -offset indent +.It Li default certificates for user or service principals, +.It Li non-default certificate requests including subject alternative names (SAN) and extended key usage (EKU) certificate extensions, for either client, server, or mixed usage. +.El +.Pp +Distinct configurations are supported for all of these cases as +shown below: +.Bd -literal -offset indent +[kdc] + enable-kx509 = yes | no + require_csr = yes | no + require_initial_kca_tickets = yes | no + realm = { + = { + kx509 = { +