diff options
Diffstat (limited to '')
-rw-r--r-- | third_party/heimdal/kdc/kx509.c | 1080 |
1 files changed, 1080 insertions, 0 deletions
diff --git a/third_party/heimdal/kdc/kx509.c b/third_party/heimdal/kdc/kx509.c new file mode 100644 index 0000000..6efd94e --- /dev/null +++ b/third_party/heimdal/kdc/kx509.c @@ -0,0 +1,1080 @@ +/* + * Copyright (c) 2006 - 2019 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the Institute nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "kdc_locl.h" +#include <hex.h> +#include <rfc2459_asn1.h> +#include <hx509.h> +#include <hx509_err.h> +#include <kx509_err.h> + +#include <stdarg.h> + +/* + * This file implements the kx509 service. + * + * The protocol, its shortcomings, and its future are described in + * lib/krb5/hx509.c. See also lib/asn1/kx509.asn1. + * + * The service handles requests, decides whether to issue a certificate, and + * does so by populating a "template" to generate a TBSCertificate and signing + * it with a configured CA issuer certificate and private key. See ca.c for + * details. + * + * A "template" is a Certificate that has ${variable} references in its + * subjectName, and may have EKUs. + * + * Some SANs may be included in issued certificates. See below. + * + * Besides future protocol improvements described in lib/krb5/hx509.c, here is + * a list of KDC functionality we'd like to add: + * + * - support templates as strings (rather than filenames) in configuration? + * - lookup an hx509 template for the client principal in its HDB entry? + * - lookup subjectName, SANs for a principal in its HDB entry + * - lookup a host-based client principal's HDB entry and add its canonical + * name / aliases as dNSName SANs + * (this would have to be if requested by the client, perhaps; see + * commentary about the protocol in lib/krb5/kx509.c) + * - add code to build a template on the fly + * + * (just SANs, with empty subjectName? + * or + * CN=component0,CN=component1,..,CN=componentN,DC=<from-REALM> + * and set KU and EKUs) + * + * Processing begins in _kdc_do_kx509(). + * + * The sequence of events in _kdc_do_kx509() is: + * + * - parse outer request + * - authenticate request + * - extract CSR and AP-REQ Authenticator authz-data elements + * - characterize request as one of + * - default client cert req (no cert exts requested, client user princ) + * - default server cert req (no cert exts requested, client service princ) + * - client cert req (cert exts requested denoting client use) + * - server cert req (cert exts requested denoting server use) + * - mixed cert req (cert exts requested denoting client and server use) + * - authorize request based only on the request's details + * - there is a default authorizer, and a plugin authorizer + * - get configuration sub-tree corresponding to the request as characterized + * - missing configuration sub-tree -> reject (we have multiple ways to + * express "no") + * - get common config params from that sub-tree + * - set TBS template and details from CSR and such + * - issue certificate by signing TBS + */ + +#ifdef KX509 + +static const unsigned char version_2_0[4] = {0 , 0, 2, 0}; + +/* + * Taste the request to see if it's a kx509 request. + */ +krb5_error_code +_kdc_try_kx509_request(kx509_req_context r) +{ + const unsigned char *p = (const void *)(uintptr_t)r->request.data; + size_t len = r->request.length; + size_t sz; + + if (len < sizeof(version_2_0)) + return -1; + if (memcmp(version_2_0, p, sizeof(version_2_0)) != 0) + return -1; + p += sizeof(version_2_0); + len -= sizeof(version_2_0); + if (len == 0) + return -1; + memset(&r->req, 0, sizeof(r->req)); + return decode_Kx509Request(p, len, &r->req, &sz); +} + +static krb5_boolean +get_bool_param(krb5_context context, + krb5_boolean def, + const char *crealm, + const char *name) +{ + krb5_boolean global_default; + + global_default = krb5_config_get_bool_default(context, NULL, def, "kdc", + name, NULL); + if (!crealm) + return global_default; + return krb5_config_get_bool_default(context, NULL, global_default, + "kdc", "realms", crealm, name, NULL); +} + +/* + * Verify the HMAC in the request. + */ +static krb5_error_code +verify_req_hash(krb5_context context, + const Kx509Request *req, + krb5_keyblock *key) +{ + unsigned char digest[SHA_DIGEST_LENGTH]; + HMAC_CTX ctx; + + if (req->pk_hash.length != sizeof(digest)) { + krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED, + "pk-hash has wrong length: %lu", + (unsigned long)req->pk_hash.length); + return KRB5KDC_ERR_PREAUTH_FAILED; + } + + HMAC_CTX_init(&ctx); + if (HMAC_Init_ex(&ctx, key->keyvalue.data, key->keyvalue.length, + EVP_sha1(), NULL) == 0) { + HMAC_CTX_cleanup(&ctx); + return krb5_enomem(context); + } + if (sizeof(digest) != HMAC_size(&ctx)) + krb5_abortx(context, "runtime error, hmac buffer wrong size in kx509"); + HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); + if (req->pk_key.length) + HMAC_Update(&ctx, req->pk_key.data, req->pk_key.length); + else + HMAC_Update(&ctx, req->authenticator.data, req->authenticator.length); + HMAC_Final(&ctx, digest, 0); + HMAC_CTX_cleanup(&ctx); + + if (ct_memcmp(req->pk_hash.data, digest, sizeof(digest)) != 0) { + krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED, + "kx509 request MAC mismatch"); + return KRB5KDC_ERR_PREAUTH_FAILED; + } + return 0; +} + +/* + * Set the HMAC in the response. + */ +static krb5_error_code +calculate_reply_hash(krb5_context context, + krb5_keyblock *key, + Kx509Response *rep) +{ + krb5_error_code ret = 0; + HMAC_CTX ctx; + + HMAC_CTX_init(&ctx); + + if (HMAC_Init_ex(&ctx, key->keyvalue.data, key->keyvalue.length, + EVP_sha1(), NULL) == 0) + ret = krb5_enomem(context); + + if (ret == 0) + ret = krb5_data_alloc(rep->hash, HMAC_size(&ctx)); + if (ret) { + HMAC_CTX_cleanup(&ctx); + return krb5_enomem(context); + } + + HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); + { + int32_t t = rep->error_code; + unsigned char encint[sizeof(t) + 1]; + size_t k; + + /* + * RFC6717 says this about how the error-code is included in the HMAC: + * + * o DER representation of the error-code exclusive of the tag and + * length, if it is present. + * + * So we use der_put_integer(), which encodes from the right. + * + * RFC6717 does not constrain the error-code's range. We assume it to + * be a 32-bit, signed integer, for which we'll need no more than 5 + * bytes. + */ + ret = der_put_integer(&encint[sizeof(encint) - 1], + sizeof(encint), &t, &k); + if (ret == 0) + HMAC_Update(&ctx, &encint[sizeof(encint)] - k, k); + } + if (rep->certificate) + HMAC_Update(&ctx, rep->certificate->data, rep->certificate->length); + if (rep->e_text) + HMAC_Update(&ctx, (unsigned char *)*rep->e_text, strlen(*rep->e_text)); + + HMAC_Final(&ctx, rep->hash->data, 0); + HMAC_CTX_cleanup(&ctx); + + return 0; +} + +static void +frees(char **s) +{ + free(*s); + *s = NULL; +} + +/* Check that a krbtgt's second component is a local realm */ +static krb5_error_code +is_local_realm(krb5_context context, + kx509_req_context reqctx, + const char *realm) +{ + krb5_error_code ret; + krb5_principal tgs; + HDB *db; + hdb_entry *ent = NULL; + + ret = krb5_make_principal(context, &tgs, realm, KRB5_TGS_NAME, realm, + NULL); + if (ret) + return ret; + if (ret == 0) + ret = _kdc_db_fetch(context, reqctx->config, tgs, HDB_F_GET_KRBTGT, + NULL, &db, &ent); + if (ent) + _kdc_free_ent(context, db, ent); + krb5_free_principal(context, tgs); + if (ret == HDB_ERR_NOENTRY || ret == HDB_ERR_NOT_FOUND_HERE) + return KRB5KRB_AP_ERR_NOT_US; + return ret; +} + +/* + * Since we're using the HDB as a keytab we have to check that the client used + * an acceptable name for the kx509 service. + * + * We accept two names: kca_service/hostname and krbtgt/REALM. + * + * We allow cross-realm requests. + * + * Maybe x-realm support should be configurable. Requiring INITIAL tickets + * does NOT preclude x-realm support! (Cross-realm TGTs can be INITIAL.) + * + * Support for specific client realms is configurable by configuring issuer + * credentials and TBS templates on a per-realm basis and configuring no + * default. But maybe we should have an explicit configuration parameter + * to enable support for clients from different realms than the service. + */ +static krb5_error_code +kdc_kx509_verify_service_principal(krb5_context context, + kx509_req_context reqctx, + krb5_principal sprincipal) +{ + krb5_error_code ret = 0; + krb5_principal principal = NULL; + char *expected = NULL; + char localhost[MAXHOSTNAMELEN]; + + if (krb5_principal_get_num_comp(context, sprincipal) != 2) + goto err; + + /* Check if sprincipal is a krbtgt/REALM name */ + if (strcmp(krb5_principal_get_comp_string(context, sprincipal, 0), + KRB5_TGS_NAME) == 0) { + const char *r = krb5_principal_get_comp_string(context, sprincipal, 1); + if ((ret = is_local_realm(context, reqctx, r))) + kdc_audit_addreason((kdc_request_t)reqctx, + "Client used wrong krbtgt for kx509"); + goto out; + } + + /* Must be hostbased kca_service name then */ + ret = gethostname(localhost, sizeof(localhost) - 1); + if (ret != 0) { + ret = errno; + kdc_log(context, reqctx->config, 0, "Failed to get local hostname"); + kdc_audit_addreason((kdc_request_t)reqctx, + "Failed to get local hostname"); + return ret; + } + localhost[sizeof(localhost) - 1] = '\0'; + + ret = krb5_make_principal(context, &principal, "", "kca_service", + localhost, NULL); + if (ret) + goto out; + + if (krb5_principal_compare_any_realm(context, sprincipal, principal)) + goto out; /* found a match */ + +err: + ret = krb5_unparse_name(context, sprincipal, &expected); + if (ret) + goto out; + + ret = KRB5KDC_ERR_SERVER_NOMATCH; + kdc_audit_addreason((kdc_request_t)reqctx, "Client used wrong kx509 " + "service principal (expected %s)", expected); + +out: + krb5_xfree(expected); + krb5_free_principal(context, principal); + + return ret; +} + +static krb5_error_code +encode_reply(krb5_context context, + kx509_req_context reqctx, + Kx509Response *r) +{ + krb5_error_code ret; + krb5_data data; + size_t size; + + reqctx->reply->data = NULL; + reqctx->reply->length = 0; + ASN1_MALLOC_ENCODE(Kx509Response, data.data, data.length, r, &size, ret); + if (ret) + return ret; + if (size != data.length) + krb5_abortx(context, "ASN1 internal error"); + + ret = krb5_data_alloc(reqctx->reply, data.length + sizeof(version_2_0)); + if (ret == 0) { + memcpy(reqctx->reply->data, version_2_0, sizeof(version_2_0)); + memcpy(((unsigned char *)reqctx->reply->data) + sizeof(version_2_0), + data.data, data.length); + } + free(data.data); + return ret; +} + +/* Make an error response, and log the error message as well */ +static krb5_error_code +mk_error_response(krb5_context context, + kx509_req_context reqctx, + int level, + int32_t code, + const char *fmt, + ...) +{ + krb5_error_code ret = code; + krb5_error_code ret2; + Kx509Response rep; + const char *msg; + char *freeme0 = NULL; + char *freeme1 = NULL; + va_list ap; + + if (code != 0) { + /* Log errors where _kdc_audit_trail() is not enough */ + if (code == ENOMEM) + level = 0; + if (level < 3) { + va_start(ap, fmt); + kdc_vlog(context, reqctx->config, level, fmt, ap); + va_end(ap); + } + + va_start(ap, fmt); + kdc_audit_vaddreason((kdc_request_t)reqctx, fmt, ap); + va_end(ap); + } + + if (!reqctx->config->enable_kx509) + code = KRB5KDC_ERR_POLICY; + + /* Make sure we only send RFC4120 and friends wire protocol error codes */ + if (code) { + if (code == KX509_ERR_NONE) { + code = 0; + } else if (code > KX509_ERR_NONE && code <= KX509_ERR_SRV_OVERLOADED) { + code -= KX509_ERR_NONE; + } else { + if (code < KRB5KDC_ERR_NONE || code >= KRB5_ERR_RCSID) + code = KRB5KRB_ERR_GENERIC; + code -= KRB5KDC_ERR_NONE; + code += kx509_krb5_error_base; + } + } + + va_start(ap, fmt); + if (vasprintf(&freeme0, fmt, ap) == -1 || freeme0 == NULL) + msg = "Could not format error message (out of memory)"; + else + msg = freeme0; + va_end(ap); + + if (!reqctx->config->enable_kx509 && + asprintf(&freeme1, "kx509 service is disabled (%s)", msg) > -1 && + freeme1 != NULL) { + msg = freeme1; + } + + rep.hash = NULL; + rep.certificate = NULL; + rep.error_code = code; + if (ALLOC(rep.e_text)) + *rep.e_text = (void *)(uintptr_t)msg; + + if (reqctx->key) { + if (ALLOC(rep.hash) != NULL && + calculate_reply_hash(context, reqctx->key, &rep)) { + free(rep.hash); + rep.hash = NULL; + } + } + + if ((ret2 = encode_reply(context, reqctx, &rep))) + ret = ret2; + if (rep.hash) + krb5_data_free(rep.hash); + free(rep.e_text); + free(rep.hash); + free(freeme0); + free(freeme1); + return ret; +} + +/* Wrap a bare public (RSA) key with a CSR (not signed it, since we can't) */ +static krb5_error_code +make_csr(krb5_context context, kx509_req_context reqctx, krb5_data *key) +{ + krb5_error_code ret; + SubjectPublicKeyInfo spki; + heim_any any; + + ret = hx509_request_init(context->hx509ctx, &reqctx->csr); + if (ret) + return ret; + + memset(&spki, 0, sizeof(spki)); + spki.subjectPublicKey.data = key->data; + spki.subjectPublicKey.length = key->length * 8; + + ret = der_copy_oid(&asn1_oid_id_pkcs1_rsaEncryption, + &spki.algorithm.algorithm); + + any.data = "\x05\x00"; + any.length = 2; + spki.algorithm.parameters = &any; + + if (ret == 0) + ret = hx509_request_set_SubjectPublicKeyInfo(context->hx509ctx, + reqctx->csr, &spki); + der_free_oid(&spki.algorithm.algorithm); + if (ret) + hx509_request_free(&reqctx->csr); + + /* + * TODO: Move a lot of the templating stuff here so we can let clients + * leave out extensions they don't want. + */ + return ret; +} + +/* Update a CSR with desired Certificate Extensions */ +static krb5_error_code +update_csr(krb5_context context, kx509_req_context reqctx, Extensions *exts) +{ + krb5_error_code ret = 0; + size_t i, k; + + if (exts == NULL) + return 0; + + for (i = 0; ret == 0 && i < exts->len; i++) { + Extension *e = &exts->val[i]; + + if (der_heim_oid_cmp(&e->extnID, &asn1_oid_id_x509_ce_keyUsage) == 0) { + KeyUsage ku; + + ret = decode_KeyUsage(e->extnValue.data, e->extnValue.length, &ku, + NULL); + if (ret) + return ret; + ret = hx509_request_set_ku(context->hx509ctx, reqctx->csr, ku); + } else if (der_heim_oid_cmp(&e->extnID, + &asn1_oid_id_x509_ce_extKeyUsage) == 0) { + ExtKeyUsage eku; + + ret = decode_ExtKeyUsage(e->extnValue.data, e->extnValue.length, + &eku, NULL); + for (k = 0; ret == 0 && k < eku.len; k++) { + ret = hx509_request_add_eku(context->hx509ctx, reqctx->csr, + &eku.val[k]); + } + free_ExtKeyUsage(&eku); + } else if (der_heim_oid_cmp(&e->extnID, + &asn1_oid_id_x509_ce_subjectAltName) == 0) { + GeneralNames san; + + ret = decode_GeneralNames(e->extnValue.data, e->extnValue.length, + &san, NULL); + for (k = 0; ret == 0 && k < san.len; k++) { + ret = hx509_request_add_GeneralName(context->hx509ctx, + reqctx->csr, &san.val[k]); + } + free_GeneralNames(&san); + } + } + if (ret) { + const char *emsg = krb5_get_error_message(context, ret); + kdc_log(context, reqctx->config, 1, + "Error handling requested extensions: %s", emsg); + kdc_audit_addreason((kdc_request_t)reqctx, + "Error handling requested extensions: %s", + emsg); + krb5_free_error_message(context, emsg); + } + return ret; +} + + +/* + * Parse the `pk_key' from the request as a CSR or raw public key, and if the + * latter, wrap it in a non-signed CSR. + */ +static krb5_error_code +get_csr(krb5_context context, kx509_req_context reqctx) +{ + krb5_error_code ret; + RSAPublicKey rsapkey; + heim_octet_string pk_key = reqctx->req.pk_key; + size_t size; + + ret = decode_Kx509CSRPlus(pk_key.data, pk_key.length, &reqctx->csr_plus, + &size); + if (ret == 0) { + reqctx->have_csr = 1; + reqctx->send_chain = 1; + + /* Parse CSR */ + ret = hx509_request_parse_der(context->hx509ctx, &reqctx->csr_plus.csr, + &reqctx->csr); + /* + * Handle any additional Certificate Extensions requested out of band + * of the CSR. + */ + if (ret == 0) + return update_csr(context, reqctx, reqctx->csr_plus.exts); + kdc_audit_addreason((kdc_request_t)reqctx, "Invalid CSR"); + return ret; + } + reqctx->send_chain = 0; + reqctx->have_csr = 0; + + /* Check if proof of possession is required by configuration */ + if (!get_bool_param(context, FALSE, reqctx->realm, "require_csr")) { + kdc_audit_addreason((kdc_request_t)reqctx, + "CSRs required but client did not send one"); + krb5_set_error_message(context, KX509_STATUS_CLIENT_USE_CSR, + "CSRs required but kx509 client did not send " + "one"); + return KX509_STATUS_CLIENT_USE_CSR; + } + + /* Attempt to decode pk_key as RSAPublicKey */ + ret = decode_RSAPublicKey(reqctx->req.pk_key.data, + reqctx->req.pk_key.length, + &rsapkey, &size); + free_RSAPublicKey(&rsapkey); + if (ret == 0 && size == reqctx->req.pk_key.length) + return make_csr(context, reqctx, &pk_key); /* Make pretend CSR */ + + /* Not an RSAPublicKey or garbage follows it */ + if (ret == 0) { + ret = KRB5KDC_ERR_NULL_KEY; + kdc_audit_addreason((kdc_request_t)reqctx, + "Request has garbage after key"); + krb5_set_error_message(context, ret, "Request has garbage after key"); + return ret; + } + + kdc_audit_addreason((kdc_request_t)reqctx, + "Could not decode CSR or RSA subject public key"); + krb5_set_error_message(context, ret, + "Could not decode CSR or RSA subject public key"); + return ret; +} + +/* + * Host-based principal _clients_ might ask for a cert for their host -- but + * which services are permitted to do that? This function answers that + * question. + */ +static int +check_authz_svc_ok(krb5_context context, const char *svc) +{ + const char *def[] = { "host", "HTTP", 0 }; + const char * const *svcs; + char **strs; + + strs = krb5_config_get_strings(context, NULL, "kdc", + "kx509_permitted_hostbased_services", NULL); + for (svcs = strs ? (const char * const *)strs : def; svcs[0]; svcs++) { + if (strcmp(svcs[0], svc) == 0) { + krb5_config_free_strings(strs); + return 1; + } + } + krb5_config_free_strings(strs); + return 0; +} + +static krb5_error_code +check_authz(krb5_context context, + kx509_req_context reqctx, + krb5_principal cprincipal) +{ + krb5_error_code ret; + const char *comp0 = krb5_principal_get_comp_string(context, cprincipal, 0); + const char *comp1 = krb5_principal_get_comp_string(context, cprincipal, 1); + unsigned int ncomp = krb5_principal_get_num_comp(context, cprincipal); + hx509_san_type san_type; + KeyUsage ku, ku_allowed; + size_t i; + const heim_oid *eku_whitelist[] = { + &asn1_oid_id_pkix_kp_serverAuth, + &asn1_oid_id_pkix_kp_clientAuth, + &asn1_oid_id_pkekuoid, + &asn1_oid_id_pkinit_ms_eku + }; + char *cprinc = NULL; + char *s = NULL; + + /* + * In the no-CSR case we'll derive cert contents from client name and its + * HDB entry -- authorization is implied. + */ + if (!reqctx->have_csr) + return 0; + ret = kdc_authorize_csr(context, reqctx->config->app, reqctx->csr, + cprincipal); + if (ret == 0) { + kdc_audit_setkv_bool((kdc_request_t)reqctx, "authorized", TRUE); + + ret = hx509_request_get_san(reqctx->csr, 0, &san_type, &s); + if (ret == 0) { + const char *san_type_s; + + /* This should be an hx509 function... */ + switch (san_type) { + case HX509_SAN_TYPE_EMAIL: san_type_s = "rfc822Name"; break; + case HX509_SAN_TYPE_DNSNAME: san_type_s = "dNSName"; break; + case HX509_SAN_TYPE_DN: san_type_s = "DN"; break; + case HX509_SAN_TYPE_REGISTERED_ID: san_type_s = "registeredID"; break; + case HX509_SAN_TYPE_XMPP: san_type_s = "xMPPName"; break; + case HX509_SAN_TYPE_PKINIT: san_type_s = "krb5PrincipalName"; break; + case HX509_SAN_TYPE_MS_UPN: san_type_s = "ms-UPN"; break; + default: san_type_s = "unknown"; break; + } + kdc_audit_addkv((kdc_request_t)reqctx, 0, "san0_type", "%s", + san_type_s); + kdc_audit_addkv((kdc_request_t)reqctx, 0, "san0", "%s", s); + } + frees(&s); + ret = hx509_request_get_eku(reqctx->csr, 0, &s); + if (ret == 0) + kdc_audit_addkv((kdc_request_t)reqctx, 0, "eku0", "%s", s); + free(s); + return 0; + } + if (ret != KRB5_PLUGIN_NO_HANDLE) { + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested extensions rejected by plugin"); + return ret; + } + + /* Default authz */ + if ((ret = krb5_unparse_name(context, cprincipal, &cprinc))) + return ret; + + for (i = 0; ret == 0; i++) { + + frees(&s); + ret = hx509_request_get_san(reqctx->csr, i, &san_type, &s); + if (ret) + break; + switch (san_type) { + case HX509_SAN_TYPE_DNSNAME: + if (ncomp != 2 || strcasecmp(comp1, s) != 0 || + strchr(s, '.') == NULL || + !check_authz_svc_ok(context, comp0)) { + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested extensions rejected by " + "default policy (dNSName SAN " + "does not match client)"); + goto eacces; + } + break; + case HX509_SAN_TYPE_PKINIT: + if (strcmp(cprinc, s) != 0) { + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested extensions rejected by " + "default policy (PKINIT SAN " + "does not match client)"); + goto eacces; + } + break; + default: + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested extensions rejected by " + "default policy (non-default SAN " + "requested)"); + goto eacces; + } + } + frees(&s); + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret) + goto out; + + for (i = 0; ret == 0; i++) { + heim_oid oid; + size_t k; + + frees(&s); + ret = hx509_request_get_eku(reqctx->csr, i, &s); + if (ret) + break; + + if ((ret = der_parse_heim_oid(s, ".", &oid))) { + goto out; + } + for (k = 0; k < sizeof(eku_whitelist)/sizeof(eku_whitelist[0]); k++) { + if (der_heim_oid_cmp(eku_whitelist[k], &oid) == 0) + break; + } + der_free_oid(&oid); + if (k == sizeof(eku_whitelist)/sizeof(eku_whitelist[0])) { + kdc_audit_addreason((kdc_request_t)reqctx, + "Requested EKU rejected by default policy"); + goto eacces; + } + } + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret) + goto out; + + memset(&ku_allowed, 0, sizeof(ku_allowed)); + ku_allowed.digitalSignature = 1; + ku_allowed.nonRepudiation = 1; + ret = hx509_request_get_ku(context->hx509ctx, reqctx->csr, &ku); + if (ret) + goto out; + if (KeyUsage2int(ku) != (KeyUsage2int(ku) & KeyUsage2int(ku_allowed))) + goto eacces; + + kdc_audit_setkv_bool((kdc_request_t)reqctx, "authorized", TRUE); + free(cprinc); + return 0; + +eacces: + ret = EACCES; + goto out2; + +out: + /* XXX Display error code */ + kdc_audit_addreason((kdc_request_t)reqctx, + "Error handling requested extensions"); +out2: + free(cprinc); + free(s); + return ret; +} + +static int +chain_add1_func(hx509_context context, void *d, hx509_cert c) +{ + heim_octet_string os; + Certificates *cs = d; + Certificate c2; + int ret; + + ret = hx509_cert_binary(context, c, &os); + if (ret) + return ret; + ret = decode_Certificate(os.data, os.length, &c2, NULL); + der_free_octet_string(&os); + if (ret) + return ret; + ret = add_Certificates(cs, &c2); + free_Certificate(&c2); + return ret; +} + +static krb5_error_code +encode_cert_and_chain(hx509_context hx509ctx, + hx509_certs certs, + krb5_data *out) +{ + krb5_error_code ret; + Certificates cs; + size_t len; + + cs.len = 0; + cs.val = 0; + + ret = hx509_certs_iter_f(hx509ctx, certs, chain_add1_func, &cs); + if (ret == 0) + ASN1_MALLOC_ENCODE(Certificates, out->data, out->length, + &cs, &len, ret); + free_Certificates(&cs); + return ret; +} + +/* + * Process a request, produce a reply. + */ + +krb5_error_code +_kdc_do_kx509(kx509_req_context r) +{ + krb5_error_code ret = 0; + krb5_ticket *ticket = NULL; + krb5_flags ap_req_options; + krb5_principal cprincipal = NULL; + krb5_principal sprincipal = NULL; + krb5_keytab id = NULL; + Kx509Response rep; + hx509_certs certs = NULL; + int is_probe = 0; + + r->csr_plus.csr.data = NULL; + r->csr_plus.exts = NULL; + r->sname = NULL; + r->cname = NULL; + r->realm = NULL; + r->key = NULL; + r->csr = NULL; + r->ac = NULL; + + /* + * In order to support authenticated error messages we defer checking + * whether the kx509 service is enabled until after accepting the AP-REQ. + */ + + krb5_data_zero(r->reply); + memset(&rep, 0, sizeof(rep)); + + if (r->req.authenticator.length == 0) { + /* + * Unauthenticated kx509 service availability probe. + * + * mk_error_response() will check whether the service is enabled and + * possibly change the error code and message. + */ + is_probe = 1; + kdc_audit_addkv((kdc_request_t)r, 0, "probe", "unauthenticated"); + ret = mk_error_response(r->context, r, 4, 0, + "kx509 service is available"); + goto out; + } + + /* Authenticate the request (consume the AP-REQ) */ + ret = krb5_kt_resolve(r->context, "HDBGET:", &id); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + ret = mk_error_response(r->context, r, 1, + KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, + "Can't open HDB/keytab for kx509: %s", + msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + ret = krb5_rd_req(r->context, + &r->ac, + &r->req.authenticator, + NULL, + id, + &ap_req_options, + &ticket); + if (ret == 0) + ret = krb5_auth_con_getkey(r->context, r->ac, &r->key); + if (ret == 0 && r->key == NULL) + ret = KRB5KDC_ERR_NULL_KEY; + /* + * Provided we got the session key, errors past this point will be + * authenticated. + */ + if (ret == 0) + ret = krb5_ticket_get_client(r->context, ticket, &cprincipal); + + /* Optional: check if Ticket is INITIAL */ + if (ret == 0 && + !ticket->ticket.flags.initial && + !get_bool_param(r->context, TRUE, + krb5_principal_get_realm(r->context, cprincipal), + "require_initial_kca_tickets")) { + ret = mk_error_response(r->context, r, 4, KRB5KDC_ERR_POLICY, + "Client used non-INITIAL tickets, but kx509 " + "service is configured to require INITIAL " + "tickets"); + goto out; + } + + if (ret == 0) + ret = krb5_unparse_name(r->context, cprincipal, &r->cname); + + /* Check that the service name is a valid kx509 service name */ + if (ret == 0) + ret = krb5_ticket_get_server(r->context, ticket, &sprincipal); + if (ret == 0) + r->realm = krb5_principal_get_realm(r->context, sprincipal); + if (ret == 0) + ret = krb5_unparse_name(r->context, sprincipal, &r->sname); + if (ret == 0) + ret = kdc_kx509_verify_service_principal(r->context, r, sprincipal); + if (ret) { + ret = mk_error_response(r->context, r, 4, ret, + "kx509 client used incorrect service name"); + goto out; + } + + /* Authenticate the rest of the request */ + ret = verify_req_hash(r->context, &r->req, r->key); + if (ret) { + ret = mk_error_response(r->context, r, 4, ret, + "Incorrect request HMAC on kx509 request"); + goto out; + } + + if (r->req.pk_key.length == 0) { + /* + * The request is an authenticated kx509 service availability probe. + * + * mk_error_response() will check whether the service is enabled and + * possibly change the error code and message. + */ + is_probe = 1; + kdc_audit_addkv((kdc_request_t)r, 0, "probe", "authenticated"); + ret = mk_error_response(r->context, r, 4, 0, + "kx509 authenticated probe request"); + goto out; + } + + /* Extract and parse CSR or a DER-encoded RSA public key */ + ret = get_csr(r->context, r); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + ret = mk_error_response(r->context, r, 3, ret, + "Failed to parse CSR: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + /* Authorize the request */ + ret = check_authz(r->context, r, cprincipal); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + ret = mk_error_response(r->context, r, 3, ret, + "Rejected by policy: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + /* Issue the certificate */ + ALLOC(rep.hash); + ALLOC(rep.certificate); + if (rep.certificate == NULL || rep.hash == NULL) { + ret = mk_error_response(r->context, r, 0, ENOMEM, + "Could allocate memory for response"); + goto out; + } + + krb5_data_zero(rep.hash); + krb5_data_zero(rep.certificate); + krb5_ticket_get_times(r->context, ticket, &r->ticket_times); + ret = kdc_issue_certificate(r->context, r->config->app, r->logf, r->csr, + cprincipal, &r->ticket_times, 0 /*req_life*/, + r->send_chain, &certs); + if (ret) { + int level = 1; + const char *msg = krb5_get_error_message(r->context, ret); + + if (ret == KRB5KDC_ERR_POLICY) + level = 4; /* _kdc_audit_trail() logs at level 3 */ + ret = mk_error_response(r->context, r, level, ret, + "Certificate isuance failed: %s", msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + ret = encode_cert_and_chain(r->context->hx509ctx, certs, rep.certificate); + if (ret) { + const char *msg = krb5_get_error_message(r->context, ret); + ret = mk_error_response(r->context, r, 1, ret, + "Could not encode certificate and chain: %s", + msg); + krb5_free_error_message(r->context, msg); + goto out; + } + + /* Authenticate the response */ + ret = calculate_reply_hash(r->context, r->key, &rep); + if (ret) { + ret = mk_error_response(r->context, r, 1, ret, + "Failed to compute response HMAC"); + goto out; + } + + /* Encode and output reply */ + ret = encode_reply(r->context, r, &rep); + if (ret) + /* Can't send an error message either in this case, surely */ + kdc_audit_addreason((kdc_request_t)r, "Could not encode response"); + +out: + hx509_certs_free(&certs); + if (ret == 0 && !is_probe) + kdc_audit_setkv_bool((kdc_request_t)r, "cert_issued", TRUE); + else + kdc_audit_setkv_bool((kdc_request_t)r, "cert_issued", FALSE); + if (r->ac) + krb5_auth_con_free(r->context, r->ac); + if (ticket) + krb5_free_ticket(r->context, ticket); + if (id) + krb5_kt_close(r->context, id); + if (sprincipal) + krb5_free_principal(r->context, sprincipal); + if (cprincipal) + krb5_free_principal(r->context, cprincipal); + if (r->key) + krb5_free_keyblock (r->context, r->key); + hx509_request_free(&r->csr); + free_Kx509CSRPlus(&r->csr_plus); + free_Kx509Response(&rep); + free_Kx509Request(&r->req); + + return ret; +} + +#endif /* KX509 */ |