/* * Copyright (C) Internet Systems Consortium, Inc. ("ISC") * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * See the COPYRIGHT file distributed with this work for additional * information regarding copyright ownership. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dst_internal.h" /* * If we're using our own SPNEGO implementation (see configure.in), * pull it in now. Otherwise, we just use whatever GSSAPI supplies. */ #if defined(GSSAPI) && defined(USE_ISC_SPNEGO) #include "spnego.h" #define gss_accept_sec_context gss_accept_sec_context_spnego #define gss_init_sec_context gss_init_sec_context_spnego #endif /* * Solaris8 apparently needs an explicit OID set, and Solaris10 needs * one for anything but Kerberos. Supplying an explicit OID set * doesn't appear to hurt anything in other implementations, so we * always use one. If we're not using our own SPNEGO implementation, * we include SPNEGO's OID. */ #ifdef GSSAPI #ifdef WIN32 #include #else #include ISC_PLATFORM_KRB5HEADER #endif static unsigned char krb5_mech_oid_bytes[] = { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02 }; #ifndef USE_ISC_SPNEGO static unsigned char spnego_mech_oid_bytes[] = { 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02 }; #endif static gss_OID_desc mech_oid_set_array[] = { { sizeof(krb5_mech_oid_bytes), krb5_mech_oid_bytes }, #ifndef USE_ISC_SPNEGO { sizeof(spnego_mech_oid_bytes), spnego_mech_oid_bytes }, #endif }; static gss_OID_set_desc mech_oid_set = { sizeof(mech_oid_set_array) / sizeof(*mech_oid_set_array), mech_oid_set_array }; #endif #define REGION_TO_GBUFFER(r, gb) \ do { \ (gb).length = (r).length; \ (gb).value = (r).base; \ } while (0) #define GBUFFER_TO_REGION(gb, r) \ do { \ (r).length = (unsigned int)(gb).length; \ (r).base = (gb).value; \ } while (0) #define RETERR(x) do { \ result = (x); \ if (result != ISC_R_SUCCESS) \ goto out; \ } while (0) #ifdef GSSAPI static inline void name_to_gbuffer(dns_name_t *name, isc_buffer_t *buffer, gss_buffer_desc *gbuffer) { dns_name_t tname, *namep; isc_region_t r; isc_result_t result; if (!dns_name_isabsolute(name)) namep = name; else { unsigned int labels; dns_name_init(&tname, NULL); labels = dns_name_countlabels(name); dns_name_getlabelsequence(name, 0, labels - 1, &tname); namep = &tname; } result = dns_name_toprincipal(namep, buffer); RUNTIME_CHECK(result == ISC_R_SUCCESS); isc_buffer_putuint8(buffer, 0); isc_buffer_usedregion(buffer, &r); REGION_TO_GBUFFER(r, *gbuffer); } static void log_cred(const gss_cred_id_t cred) { OM_uint32 gret, minor, lifetime; gss_name_t gname; gss_buffer_desc gbuffer; gss_cred_usage_t usage; const char *usage_text; char buf[1024]; gret = gss_inquire_cred(&minor, cred, &gname, &lifetime, &usage, NULL); if (gret != GSS_S_COMPLETE) { gss_log(3, "failed gss_inquire_cred: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); return; } gret = gss_display_name(&minor, gname, &gbuffer, NULL); if (gret != GSS_S_COMPLETE) gss_log(3, "failed gss_display_name: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); else { switch (usage) { case GSS_C_BOTH: usage_text = "GSS_C_BOTH"; break; case GSS_C_INITIATE: usage_text = "GSS_C_INITIATE"; break; case GSS_C_ACCEPT: usage_text = "GSS_C_ACCEPT"; break; default: usage_text = "???"; } gss_log(3, "gss cred: \"%s\", %s, %lu", (char *)gbuffer.value, usage_text, (unsigned long)lifetime); } if (gret == GSS_S_COMPLETE) { if (gbuffer.length != 0U) { gret = gss_release_buffer(&minor, &gbuffer); if (gret != GSS_S_COMPLETE) gss_log(3, "failed gss_release_buffer: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); } } gret = gss_release_name(&minor, &gname); if (gret != GSS_S_COMPLETE) gss_log(3, "failed gss_release_name: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); } #endif #ifdef GSSAPI /* * check for the most common configuration errors. * * The errors checked for are: * - tkey-gssapi-credential doesn't start with DNS/ * - the default realm in /etc/krb5.conf and the * tkey-gssapi-credential bind config option don't match * * Note that if tkey-gssapi-keytab is set then these configure checks * are not performed, and runtime errors from gssapi are used instead */ static void check_config(const char *gss_name) { const char *p; krb5_context krb5_ctx; char *krb5_realm_name = NULL; if (strncasecmp(gss_name, "DNS/", 4) != 0) { gss_log(ISC_LOG_ERROR, "tkey-gssapi-credential (%s) " "should start with 'DNS/'", gss_name); return; } if (krb5_init_context(&krb5_ctx) != 0) { gss_log(ISC_LOG_ERROR, "Unable to initialise krb5 context"); return; } if (krb5_get_default_realm(krb5_ctx, &krb5_realm_name) != 0) { gss_log(ISC_LOG_ERROR, "Unable to get krb5 default realm"); krb5_free_context(krb5_ctx); return; } p = strchr(gss_name, '@'); if (p == NULL) { gss_log(ISC_LOG_ERROR, "badly formatted " "tkey-gssapi-credentials (%s)", gss_name); krb5_free_context(krb5_ctx); return; } if (strcasecmp(p + 1, krb5_realm_name) != 0) { gss_log(ISC_LOG_ERROR, "default realm from krb5.conf (%s) " "does not match tkey-gssapi-credential (%s)", krb5_realm_name, gss_name); krb5_free_context(krb5_ctx); return; } krb5_free_context(krb5_ctx); } #endif isc_result_t dst_gssapi_acquirecred(dns_name_t *name, bool initiate, gss_cred_id_t *cred) { #ifdef GSSAPI isc_result_t result; isc_buffer_t namebuf; gss_name_t gname; gss_buffer_desc gnamebuf; unsigned char array[DNS_NAME_MAXTEXT + 1]; OM_uint32 gret, minor; OM_uint32 lifetime; gss_cred_usage_t usage; char buf[1024]; REQUIRE(cred != NULL && *cred == NULL); /* * XXXSRA In theory we could use GSS_C_NT_HOSTBASED_SERVICE * here when we're in the acceptor role, which would let us * default the hostname and use a compiled in default service * name of "DNS", giving one less thing to configure in * named.conf. Unfortunately, this creates a circular * dependency due to DNS-based realm lookup in at least one * GSSAPI implementation (Heimdal). Oh well. */ if (name != NULL) { isc_buffer_init(&namebuf, array, sizeof(array)); name_to_gbuffer(name, &namebuf, &gnamebuf); gret = gss_import_name(&minor, &gnamebuf, GSS_C_NO_OID, &gname); if (gret != GSS_S_COMPLETE) { check_config((char *)array); gss_log(3, "failed gss_import_name: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); return (ISC_R_FAILURE); } } else gname = NULL; /* Get the credentials. */ if (gname != NULL) gss_log(3, "acquiring credentials for %s", (char *)gnamebuf.value); else { /* XXXDCL does this even make any sense? */ gss_log(3, "acquiring credentials for ?"); } if (initiate) usage = GSS_C_INITIATE; else usage = GSS_C_ACCEPT; gret = gss_acquire_cred(&minor, gname, GSS_C_INDEFINITE, &mech_oid_set, usage, cred, NULL, &lifetime); if (gret != GSS_S_COMPLETE) { gss_log(3, "failed to acquire %s credentials for %s: %s", initiate ? "initiate" : "accept", (gname != NULL) ? (char *)gnamebuf.value : "?", gss_error_tostring(gret, minor, buf, sizeof(buf))); if (gname != NULL) check_config((char *)array); result = ISC_R_FAILURE; goto cleanup; } gss_log(4, "acquired %s credentials for %s", initiate ? "initiate" : "accept", (gname != NULL) ? (char *)gnamebuf.value : "?"); log_cred(*cred); result = ISC_R_SUCCESS; cleanup: if (gname != NULL) { gret = gss_release_name(&minor, &gname); if (gret != GSS_S_COMPLETE) gss_log(3, "failed gss_release_name: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); } return (result); #else REQUIRE(cred != NULL && *cred == NULL); UNUSED(name); UNUSED(initiate); UNUSED(cred); return (ISC_R_NOTIMPLEMENTED); #endif } bool dst_gssapi_identitymatchesrealmkrb5(const dns_name_t *signer, const dns_name_t *name, const dns_name_t *realm, bool subdomain) { #ifdef GSSAPI char sbuf[DNS_NAME_FORMATSIZE]; char rbuf[DNS_NAME_FORMATSIZE]; char *sname; char *rname; isc_buffer_t buffer; isc_result_t result; /* * It is far, far easier to write the names we are looking at into * a string, and do string operations on them. */ isc_buffer_init(&buffer, sbuf, sizeof(sbuf)); result = dns_name_toprincipal(signer, &buffer); RUNTIME_CHECK(result == ISC_R_SUCCESS); isc_buffer_putuint8(&buffer, 0); dns_name_format(realm, rbuf, sizeof(rbuf)); /* * Find the realm portion. This is the part after the @. If it * does not exist, we don't have something we like, so we fail our * compare. */ rname = strchr(sbuf, '@'); if (rname == NULL) return (false); *rname = '\0'; rname++; if (strcmp(rname, rbuf) != 0) { return (false); } /* * Find the host portion of the signer's name. We do this by * searching for the first / character. We then check to make * certain the instance name is "host" * * This will work for * host/example.com@EXAMPLE.COM */ sname = strchr(sbuf, '/'); if (sname == NULL) return (false); *sname = '\0'; sname++; if (strcmp(sbuf, "host") != 0) return (false); /* * If name is non NULL check that it matches against the * machine name as expected. */ if (name != NULL) { dns_fixedname_t fixed; dns_name_t *machine; machine = dns_fixedname_initname(&fixed); result = dns_name_fromstring(machine, sname, 0, NULL); if (result != ISC_R_SUCCESS) { return (false); } if (subdomain) { return (dns_name_issubdomain(name, machine)); } return (dns_name_equal(name, machine)); } return (true); #else UNUSED(signer); UNUSED(name); UNUSED(realm); UNUSED(subdomain); return (false); #endif } bool dst_gssapi_identitymatchesrealmms(const dns_name_t *signer, const dns_name_t *name, const dns_name_t *realm, bool subdomain) { #ifdef GSSAPI char sbuf[DNS_NAME_FORMATSIZE]; char rbuf[DNS_NAME_FORMATSIZE]; char *sname; char *rname; isc_buffer_t buffer; isc_result_t result; /* * It is far, far easier to write the names we are looking at into * a string, and do string operations on them. */ isc_buffer_init(&buffer, sbuf, sizeof(sbuf)); result = dns_name_toprincipal(signer, &buffer); RUNTIME_CHECK(result == ISC_R_SUCCESS); isc_buffer_putuint8(&buffer, 0); dns_name_format(realm, rbuf, sizeof(rbuf)); /* * Find the realm portion. This is the part after the @. If it * does not exist, we don't have something we like, so we fail our * compare. */ rname = strchr(sbuf, '@'); if (rname == NULL) return (false); sname = strchr(sbuf, '$'); if (sname == NULL) return (false); /* * Verify that the $ and @ follow one another. */ if (rname - sname != 1) return (false); /* * Find the host portion of the signer's name. Zero out the $ so * it terminates the signer's name, and skip past the @ for * the realm. * * All service principals in Microsoft format seem to be in * machinename$@EXAMPLE.COM * format. */ rname++; *sname = '\0'; if (strcmp(rname, rbuf) != 0) { return (false); } /* * Now, we check that the realm matches (case sensitive) and that * 'name' matches against 'machinename' qualified with 'realm'. */ if (name != NULL) { dns_fixedname_t fixed; dns_name_t *machine; machine = dns_fixedname_initname(&fixed); result = dns_name_fromstring2(machine, sbuf, realm, 0, NULL); if (result != ISC_R_SUCCESS) { return (false); } if (subdomain) { return (dns_name_issubdomain(name, machine)); } return (dns_name_equal(name, machine)); } return (true); #else UNUSED(signer); UNUSED(name); UNUSED(realm); UNUSED(subdomain); return (false); #endif } isc_result_t dst_gssapi_releasecred(gss_cred_id_t *cred) { #ifdef GSSAPI OM_uint32 gret, minor; char buf[1024]; REQUIRE(cred != NULL && *cred != NULL); gret = gss_release_cred(&minor, cred); if (gret != GSS_S_COMPLETE) { /* Log the error, but still free the credential's memory */ gss_log(3, "failed releasing credential: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); } *cred = NULL; return(ISC_R_SUCCESS); #else UNUSED(cred); return (ISC_R_NOTIMPLEMENTED); #endif } #ifdef GSSAPI /* * Format a gssapi error message info into a char ** on the given memory * context. This is used to return gssapi error messages back up the * call chain for reporting to the user. */ static void gss_err_message(isc_mem_t *mctx, uint32_t major, uint32_t minor, char **err_message) { char buf[1024]; char *estr; if (err_message == NULL || mctx == NULL) { /* the caller doesn't want any error messages */ return; } estr = gss_error_tostring(major, minor, buf, sizeof(buf)); if (estr != NULL) (*err_message) = isc_mem_strdup(mctx, estr); } #endif isc_result_t dst_gssapi_initctx(dns_name_t *name, isc_buffer_t *intoken, isc_buffer_t *outtoken, gss_ctx_id_t *gssctx, isc_mem_t *mctx, char **err_message) { #ifdef GSSAPI isc_region_t r; isc_buffer_t namebuf; gss_name_t gname; OM_uint32 gret, minor, ret_flags, flags; gss_buffer_desc gintoken, *gintokenp, gouttoken = GSS_C_EMPTY_BUFFER; isc_result_t result; gss_buffer_desc gnamebuf; unsigned char array[DNS_NAME_MAXTEXT + 1]; /* Client must pass us a valid gss_ctx_id_t here */ REQUIRE(gssctx != NULL); REQUIRE(mctx != NULL); isc_buffer_init(&namebuf, array, sizeof(array)); name_to_gbuffer(name, &namebuf, &gnamebuf); /* Get the name as a GSS name */ gret = gss_import_name(&minor, &gnamebuf, GSS_C_NO_OID, &gname); if (gret != GSS_S_COMPLETE) { gss_err_message(mctx, gret, minor, err_message); result = ISC_R_FAILURE; goto out; } if (intoken != NULL) { /* Don't call gss_release_buffer for gintoken! */ REGION_TO_GBUFFER(*intoken, gintoken); gintokenp = &gintoken; } else { gintokenp = NULL; } /* * Note that we don't set GSS_C_SEQUENCE_FLAG as Windows DNS * servers don't like it. */ flags = GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG; gret = gss_init_sec_context(&minor, GSS_C_NO_CREDENTIAL, gssctx, gname, GSS_SPNEGO_MECHANISM, flags, 0, NULL, gintokenp, NULL, &gouttoken, &ret_flags, NULL); if (gret != GSS_S_COMPLETE && gret != GSS_S_CONTINUE_NEEDED) { gss_err_message(mctx, gret, minor, err_message); if (err_message != NULL && *err_message != NULL) gss_log(3, "Failure initiating security context: %s", *err_message); else gss_log(3, "Failure initiating security context"); result = ISC_R_FAILURE; goto out; } /* * XXXSRA Not handled yet: RFC 3645 3.1.1: check ret_flags * MUTUAL and INTEG flags, fail if either not set. */ /* * RFC 2744 states the a valid output token has a non-zero length. */ if (gouttoken.length != 0U) { GBUFFER_TO_REGION(gouttoken, r); RETERR(isc_buffer_copyregion(outtoken, &r)); } if (gret == GSS_S_COMPLETE) result = ISC_R_SUCCESS; else result = DNS_R_CONTINUE; out: if (gouttoken.length != 0U) (void)gss_release_buffer(&minor, &gouttoken); (void)gss_release_name(&minor, &gname); return (result); #else UNUSED(name); UNUSED(intoken); UNUSED(outtoken); UNUSED(gssctx); UNUSED(mctx); UNUSED(err_message); return (ISC_R_NOTIMPLEMENTED); #endif } isc_result_t dst_gssapi_acceptctx(gss_cred_id_t cred, const char *gssapi_keytab, isc_region_t *intoken, isc_buffer_t **outtoken, gss_ctx_id_t *ctxout, dns_name_t *principal, isc_mem_t *mctx) { #ifdef GSSAPI isc_region_t r; isc_buffer_t namebuf; gss_buffer_desc gnamebuf = GSS_C_EMPTY_BUFFER, gintoken, gouttoken = GSS_C_EMPTY_BUFFER; OM_uint32 gret, minor; gss_ctx_id_t context = GSS_C_NO_CONTEXT; gss_name_t gname = NULL; isc_result_t result; char buf[1024]; REQUIRE(outtoken != NULL && *outtoken == NULL); REGION_TO_GBUFFER(*intoken, gintoken); if (*ctxout == NULL) context = GSS_C_NO_CONTEXT; else context = *ctxout; if (gssapi_keytab != NULL) { #if defined(ISC_PLATFORM_GSSAPI_KRB5_HEADER) || defined(WIN32) gret = gsskrb5_register_acceptor_identity(gssapi_keytab); if (gret != GSS_S_COMPLETE) { gss_log(3, "failed " "gsskrb5_register_acceptor_identity(%s): %s", gssapi_keytab, gss_error_tostring(gret, 0, buf, sizeof(buf))); return (DNS_R_INVALIDTKEY); } #else /* * Minimize memory leakage by only setting KRB5_KTNAME * if it needs to change. */ const char *old = getenv("KRB5_KTNAME"); if (old == NULL || strcmp(old, gssapi_keytab) != 0) { size_t size; char *kt; size = strlen(gssapi_keytab) + 13; kt = malloc(size); if (kt == NULL) return (ISC_R_NOMEMORY); snprintf(kt, size, "KRB5_KTNAME=%s", gssapi_keytab); if (putenv(kt) != 0) return (ISC_R_NOMEMORY); } #endif } log_cred(cred); gret = gss_accept_sec_context(&minor, &context, cred, &gintoken, GSS_C_NO_CHANNEL_BINDINGS, &gname, NULL, &gouttoken, NULL, NULL, NULL); result = ISC_R_FAILURE; switch (gret) { case GSS_S_COMPLETE: case GSS_S_CONTINUE_NEEDED: break; case GSS_S_DEFECTIVE_TOKEN: case GSS_S_DEFECTIVE_CREDENTIAL: case GSS_S_BAD_SIG: case GSS_S_DUPLICATE_TOKEN: case GSS_S_OLD_TOKEN: case GSS_S_NO_CRED: case GSS_S_CREDENTIALS_EXPIRED: case GSS_S_BAD_BINDINGS: case GSS_S_NO_CONTEXT: case GSS_S_BAD_MECH: case GSS_S_FAILURE: result = DNS_R_INVALIDTKEY; /* fall through */ default: gss_log(3, "failed gss_accept_sec_context: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); return (result); } if (gouttoken.length > 0U) { RETERR(isc_buffer_allocate(mctx, outtoken, (unsigned int)gouttoken.length)); GBUFFER_TO_REGION(gouttoken, r); RETERR(isc_buffer_copyregion(*outtoken, &r)); (void)gss_release_buffer(&minor, &gouttoken); } if (gret == GSS_S_COMPLETE) { gret = gss_display_name(&minor, gname, &gnamebuf, NULL); if (gret != GSS_S_COMPLETE) { gss_log(3, "failed gss_display_name: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); RETERR(ISC_R_FAILURE); } /* * Compensate for a bug in Solaris8's implementation * of gss_display_name(). Should be harmless in any * case, since principal names really should not * contain null characters. */ if (gnamebuf.length > 0U && ((char *)gnamebuf.value)[gnamebuf.length - 1] == '\0') gnamebuf.length--; gss_log(3, "gss-api source name (accept) is %.*s", (int)gnamebuf.length, (char *)gnamebuf.value); GBUFFER_TO_REGION(gnamebuf, r); isc_buffer_init(&namebuf, r.base, r.length); isc_buffer_add(&namebuf, r.length); RETERR(dns_name_fromtext(principal, &namebuf, dns_rootname, 0, NULL)); if (gnamebuf.length != 0U) { gret = gss_release_buffer(&minor, &gnamebuf); if (gret != GSS_S_COMPLETE) gss_log(3, "failed gss_release_buffer: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); } } else result = DNS_R_CONTINUE; *ctxout = context; out: if (gname != NULL) { gret = gss_release_name(&minor, &gname); if (gret != GSS_S_COMPLETE) gss_log(3, "failed gss_release_name: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); } return (result); #else UNUSED(cred); UNUSED(gssapi_keytab); UNUSED(intoken); UNUSED(outtoken); UNUSED(ctxout); UNUSED(principal); UNUSED(mctx); return (ISC_R_NOTIMPLEMENTED); #endif } isc_result_t dst_gssapi_deletectx(isc_mem_t *mctx, gss_ctx_id_t *gssctx) { #ifdef GSSAPI OM_uint32 gret, minor; char buf[1024]; UNUSED(mctx); REQUIRE(gssctx != NULL && *gssctx != NULL); /* Delete the context from the GSS provider */ gret = gss_delete_sec_context(&minor, gssctx, GSS_C_NO_BUFFER); if (gret != GSS_S_COMPLETE) { /* Log the error, but still free the context's memory */ gss_log(3, "Failure deleting security context %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); } return(ISC_R_SUCCESS); #else UNUSED(mctx); UNUSED(gssctx); return (ISC_R_NOTIMPLEMENTED); #endif } char * gss_error_tostring(uint32_t major, uint32_t minor, char *buf, size_t buflen) { #ifdef GSSAPI gss_buffer_desc msg_minor = GSS_C_EMPTY_BUFFER, msg_major = GSS_C_EMPTY_BUFFER; OM_uint32 msg_ctx, minor_stat; /* Handle major status */ msg_ctx = 0; (void)gss_display_status(&minor_stat, major, GSS_C_GSS_CODE, GSS_C_NULL_OID, &msg_ctx, &msg_major); /* Handle minor status */ msg_ctx = 0; (void)gss_display_status(&minor_stat, minor, GSS_C_MECH_CODE, GSS_C_NULL_OID, &msg_ctx, &msg_minor); snprintf(buf, buflen, "GSSAPI error: Major = %s, Minor = %s.", (char *)msg_major.value, (char *)msg_minor.value); if (msg_major.length != 0U) (void)gss_release_buffer(&minor_stat, &msg_major); if (msg_minor.length != 0U) (void)gss_release_buffer(&minor_stat, &msg_minor); return(buf); #else snprintf(buf, buflen, "GSSAPI error: Major = %u, Minor = %u.", major, minor); return (buf); #endif } void gss_log(int level, const char *fmt, ...) { va_list ap; va_start(ap, fmt); isc_log_vwrite(dns_lctx, DNS_LOGCATEGORY_GENERAL, DNS_LOGMODULE_TKEY, ISC_LOG_DEBUG(level), fmt, ap); va_end(ap); } /*! \file */