diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:20:00 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:20:00 +0000 |
commit | 8daa83a594a2e98f39d764422bfbdbc62c9efd44 (patch) | |
tree | 4099e8021376c7d8c05bdf8503093d80e9c7bad0 /third_party/heimdal/kdc/ipc_csr_authorizer.c | |
parent | Initial commit. (diff) | |
download | samba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.tar.xz samba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.zip |
Adding upstream version 2:4.20.0+dfsg.upstream/2%4.20.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/heimdal/kdc/ipc_csr_authorizer.c')
-rw-r--r-- | third_party/heimdal/kdc/ipc_csr_authorizer.c | 681 |
1 files changed, 681 insertions, 0 deletions
diff --git a/third_party/heimdal/kdc/ipc_csr_authorizer.c b/third_party/heimdal/kdc/ipc_csr_authorizer.c new file mode 100644 index 0000000..f2ce03c --- /dev/null +++ b/third_party/heimdal/kdc/ipc_csr_authorizer.c @@ -0,0 +1,681 @@ +/* + * Copyright (c) 2019 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the Institute nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * This plugin authorizes requested certificate SANs and EKUs by calling a + * service over IPC (Unix domain sockets on Linux/BSD/Illumos). + * + * The IPC protocol is request/response, with requests and responses sent as + * + * <length><string> + * + * where the <length> is 4 bytes, unsigned binary in network byte order, and + * <string> is an array of <length> bytes and does NOT include a NUL + * terminator. + * + * Requests are of the form: + * + * check <princ> <exttype>=<extvalue> ... + * + * where <princ> is a URL-escaped principal name, <exttype> is one of: + * + * - san_pkinit + * - san_xmpp + * - san_email + * - san_ms_upn + * - san_dnsname + * - eku + * + * and <extvalue> is a URL-escaped string representation of the SAN or OID. + * + * OIDs are in the form 1.2.3.4.5.6. + * + * Only characters other than alphanumeric, '@', '.', '-', '_', and '/' are + * URL-encoded. + * + * Responses are any of: + * + * - granted + * - denied + * - error message + * + * Example: + * + * C->S: check jane@TEST.H5L.SE san_dnsname=jane.foo.test.h5l.se eku=1.3.6.1.5.5.7.3.1 + * S->C: granted + * + * Only digitalSignature and nonRepudiation key usages are allowed. Requested + * key usages are not sent to the CSR authorizer IPC server. + */ + +#define _GNU_SOURCE 1 + +#include <sys/types.h> +#include <sys/stat.h> +#include <ctype.h> +#include <errno.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include <roken.h> +#include <heim-ipc.h> +#include <krb5.h> +#include <hx509.h> +#include <kdc.h> +#include <common_plugin.h> +#include <csr_authorizer_plugin.h> + +/* + * string_encode_sz() and string_encode() encode principal names and such to be + * safe for use in our IPC text messages. They function very much like URL + * encoders, but '~' also gets encoded, and '.' and '@' do not. + * + * An unescaper is not needed here. + */ +static size_t +string_encode_sz(const char *in) +{ + size_t sz = strlen(in); + + while (*in) { + char c = *(in++); + + switch (c) { + case '@': + case '.': + case '-': + case '_': + case '/': + continue; + default: + if (isalnum((unsigned char)c)) + continue; + sz += 2; + } + } + return sz; +} + +static char * +string_encode(const char *in) +{ + size_t len = strlen(in); + size_t sz = string_encode_sz(in); + size_t i, k; + char *s; + + if ((s = malloc(sz + 1)) == NULL) + return NULL; + s[sz] = '\0'; + + for (i = k = 0; i < len; i++) { + unsigned char c = ((const unsigned char *)in)[i]; + + switch (c) { + case '@': + case '.': + case '-': + case '_': + case '/': + s[k++] = c; + break; + default: + if (isalnum(c)) { + s[k++] = c; + } else { + s[k++] = '%'; + s[k++] = "0123456789abcdef"[(c&0xff)>>4]; + s[k++] = "0123456789abcdef"[(c&0x0f)]; + } + } + } + return s; +} + +static int +cmd_append(struct rk_strpool **cmd, const char *s0, ...) +{ + va_list ap; + const char *arg; + int ret = 0; + + if ((*cmd = rk_strpoolprintf(*cmd, "%s", s0)) == NULL) + return ENOMEM; + + va_start(ap, s0); + while ((arg = va_arg(ap, const char *))) { + char *s; + + if ((s = string_encode(arg)) == NULL) { + rk_strpoolfree(*cmd); + *cmd = NULL; + ret = ENOMEM; + goto out; + } + *cmd = rk_strpoolprintf(*cmd, "%s", s); + free(s); + if (*cmd == NULL) { + ret = ENOMEM; + goto out; + } + } + + out: + va_end(ap); + return ret; +} + +/* Like strpbrk(), but from the end of the string */ +static char * +strrpbrk(char *s, const char *accept) +{ + char *last = NULL; + char *p = s; + + do { + p = strpbrk(p, accept); + if (p != NULL) { + last = p; + p++; + } + } while (p != NULL); + return last; +} + +/* + * For /get-tgts we need to support partial authorization of requests. The + * hx509_request APIs support that. + * + * Here we just step through the IPC server's response and mark the + * corresponding request elements authorized so that /get-tgts can issue or not + * issue TGTs according to which requested principals are authorized and which + * are not. + */ +static int +mark_piecemeal_authorized(krb5_context context, + hx509_request csr, + heim_octet_string *rep) +{ + size_t san_idx = 0; + size_t eku_idx = 0; + char *s, *p, *rep2, *tok, *next = NULL; + int slow_path = 0; + int partial = 0; + int ret = 0; + + /* We have a data, but we want a C string */ + if ((rep2 = strndup(rep->data, rep->length)) == NULL) + return krb5_enomem(context); + + /* The first token should be "denied"; skip it */ + if ((s = strchr(rep2, ' ')) == NULL) { + free(rep2); + return EACCES; + } + s++; + + while ((tok = strtok_r(s, ",", &next))) { + hx509_san_type san_type, san_type2; + char *s2 = NULL; + + s = NULL; /* for strtok_r() */ + + if (strncmp(tok, "eku=", sizeof("eku=") -1) == 0) { + /* + * Very simplistic handling of partial authz for EKUs: + * + * - denial of an EKU -> deny the whole request + * - else below mark all EKUs approved + */ + if (strstr(tok, ":denied")) { + krb5_set_error_message(context, EACCES, "CSR denied because " + "EKU denied: %s", tok); + ret = EACCES; + break; + } + continue; + } + + /* + * For SANs we check that the nth SAN in the response matches the nth + * SAN in the hx509_request. + */ + + if (strncmp(tok, "san_pkinit=", sizeof("san_pkinit=") - 1) == 0) { + tok += sizeof("san_pkinit=") - 1; + san_type = HX509_SAN_TYPE_PKINIT; + } else if (strncmp(tok, "san_dnsname=", sizeof("san_dnsname=") -1) == 0) { + tok += sizeof("san_dnsname=") - 1; + san_type = HX509_SAN_TYPE_DNSNAME; + } else if (strncmp(tok, "san_email=", sizeof("san_email=") -1) == 0) { + tok += sizeof("san_email=") - 1; + san_type = HX509_SAN_TYPE_EMAIL; + } else if (strncmp(tok, "san_xmpp=", sizeof("san_xmpp=") -1) == 0) { + tok += sizeof("san_xmpp=") - 1; + san_type = HX509_SAN_TYPE_XMPP; + } else if (strncmp(tok, "san_ms_upn=", sizeof("san_ms_upn=") -1) == 0) { + tok += sizeof("san_ms_upn=") - 1; + san_type = HX509_SAN_TYPE_MS_UPN; + } else { + krb5_set_error_message(context, EACCES, "CSR denied because could " + "not parse token in response: %s", tok); + ret = EACCES; + break; + } + + /* + * This token has to end in ":granted" or ":denied". Using our + * `strrpbrk()' means we can deal with principals names that have ':' + * in them. + */ + if ((p = strrpbrk(tok, ":")) == NULL) { + san_idx++; + continue; + } + *(p++) = '\0'; + + /* Now we get the nth SAN from the authorization */ + ret = hx509_request_get_san(csr, san_idx, &san_type2, &s2); + if (ret == HX509_NO_ITEM) { + /* See below */ + slow_path = 1; + break; + } + + /* And we check that it matches the SAN in this token */ + if (ret == 0) { + if (san_type != san_type2 || + strcmp(tok, s2) != 0) { + /* + * We expect the tokens in the reply to be in the same order as + * in the request. If not, we must take a slow path where we + * have to sort requests and responses then iterate them in + * order. + */ + slow_path = 1; + hx509_xfree(s2); + break; + } + hx509_xfree(s2); + + if (strcmp(p, "granted") == 0) { + ret = hx509_request_authorize_san(csr, san_idx); + } else { + partial = 1; + ret = hx509_request_reject_san(csr, san_idx); + } + if (ret) + break; + } + san_idx++; + } + + if (slow_path) { + /* + * FIXME? Implement the slow path? + * + * Basically, we'd get all the SANs from the request into an array of + * {SAN, index} and sort that array, then all the SANs from the + * response into an array and sort it, then step a cursor through both, + * using the index from the first to mark SANs in the request + * authorized or rejected. + */ + krb5_set_error_message(context, EACCES, "CSR denied because " + "authorizer service did not include all " + "piecemeal grants/denials in order"); + ret = EACCES; + } + + /* Mark all the EKUs authorized */ + for (eku_idx = 0; ret == 0; eku_idx++) + ret = hx509_request_authorize_eku(csr, eku_idx); + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret == 0 && partial) { + krb5_set_error_message(context, EACCES, "CSR partially authorized"); + ret = EACCES; + } + + free(rep2); + return ret; +} + +static krb5_error_code mark_authorized(hx509_request); + +static int +call_svc(krb5_context context, + heim_ipc ipc, + hx509_request csr, + const char *cmd, + int piecemeal_check_ok) +{ + heim_octet_string req, resp; + int ret; + + req.data = (void *)(uintptr_t)cmd; + req.length = strlen(cmd); + resp.length = 0; + resp.data = NULL; + ret = heim_ipc_call(ipc, &req, &resp, NULL); + + /* Check for all granted case */ + if (ret == 0 && + resp.length == sizeof("granted") - 1 && + strncasecmp(resp.data, "granted", sizeof("granted") - 1) == 0) { + free(resp.data); + return mark_authorized(csr); /* Full approval */ + } + + /* Check for "denied ..." piecemeal authorization case */ + if ((ret == 0 || ret == EACCES || ret == KRB5_PLUGIN_NO_HANDLE) && + piecemeal_check_ok && + resp.length > sizeof("denied") - 1 && + strncasecmp(resp.data, "denied", sizeof("denied") - 1) == 0) { + /* Piecemeal authorization */ + ret = mark_piecemeal_authorized(context, csr, &resp); + + /* mark_piecemeal_authorized() should return EACCES; just in case: */ + if (ret == 0) + ret = EACCES; + free(resp.data); + return ret; + } + + /* All other failure cases */ + + if (resp.data == NULL || resp.length == 0) { + krb5_set_error_message(context, ret, "CSR authorizer IPC service " + "failed silently"); + free(resp.data); + return EACCES; + } + + if (resp.length == sizeof("ignore") - 1 && + strncasecmp(resp.data, "ignore", sizeof("ignore") - 1) == 0) { + /* + * In this case the server is saying "I can't handle this request, try + * some other authorizer plugin". + */ + free(resp.data); + return KRB5_PLUGIN_NO_HANDLE; + } + + if (resp.length == sizeof("denied") - 1 && + strncasecmp(resp.data, "denied", sizeof("denied") - 1) == 0) { + krb5_set_error_message(context, ret, "CSR authorizer rejected %s", + cmd); + free(resp.data); + return EACCES; + } + + if (resp.length > INT_MAX) + krb5_set_error_message(context, ret, "CSR authorizer rejected %s", cmd); + else + krb5_set_error_message(context, ret, "CSR authorizer rejected %s: %.*s", + cmd, resp.length, resp.data); + + free(resp.data); + return ret; +} + +static void +frees(char **s) +{ + free(*s); + *s = NULL; +} + +static krb5_error_code +mark_authorized(hx509_request csr) +{ + size_t i; + char *s; + int ret = 0; + + for (i = 0; ret == 0; i++) { + ret = hx509_request_get_eku(csr, i, &s); + if (ret == 0) + hx509_request_authorize_eku(csr, i); + frees(&s); + } + if (ret == HX509_NO_ITEM) + ret = 0; + + for (i = 0; ret == 0; i++) { + hx509_san_type san_type; + ret = hx509_request_get_san(csr, i, &san_type, &s); + if (ret == 0) + hx509_request_authorize_san(csr, i); + frees(&s); + } + return ret == HX509_NO_ITEM ? 0 : ret; +} + +static KRB5_LIB_CALL krb5_error_code +authorize(void *ctx, + krb5_context context, + const char *app, + hx509_request csr, + krb5_const_principal client, + krb5_boolean *result) +{ + struct rk_strpool *cmd = NULL; + krb5_error_code ret; + hx509_context hx509ctx = NULL; + heim_ipc ipc = NULL; + const char *svc; + KeyUsage ku; + size_t i; + char *princ = NULL; + char *s = NULL; + int do_check = 0; + int piecemeal_check_ok = 1; + + if ((svc = krb5_config_get_string_default(context, NULL, + "ANY:org.h5l.csr_authorizer", + app ? app : "kdc", + "ipc_csr_authorizer", "service", + NULL)) == NULL) + return KRB5_PLUGIN_NO_HANDLE; + + if ((ret = heim_ipc_init_context(svc, &ipc))) { + /* + * If the IPC authorizer is optional, then fallback on whatever is + * next. + */ + if (krb5_config_get_bool_default(context, NULL, FALSE, + app ? app : "kdc", + "ipc_csr_authorizer", "optional", + NULL)) + return KRB5_PLUGIN_NO_HANDLE; + krb5_set_error_message(context, ret, "Could not set up IPC client " + "end-point for service %s", svc); + return ret; + } + + if ((ret = hx509_context_init(&hx509ctx))) + goto out; + + if ((ret = krb5_unparse_name(context, client, &princ))) + goto out; + + if ((ret = cmd_append(&cmd, "check ", princ, NULL))) + goto enomem; + frees(&princ); + + for (i = 0; ret == 0; i++) { + hx509_san_type san_type; + size_t p; + + ret = hx509_request_get_san(csr, i, &san_type, &s); + if (ret) + break; + + /* + * We cannot do a piecemeal check if any of the SANs could make the + * response ambiguous. + */ + p = strcspn(s, ",= "); + if (s[p] != '\0') + piecemeal_check_ok = 0; + if (piecemeal_check_ok && strstr(s, ":granted") != NULL) + piecemeal_check_ok = 0; + + switch (san_type) { + case HX509_SAN_TYPE_EMAIL: + if ((ret = cmd_append(&cmd, " san_email=", s, NULL))) + goto enomem; + do_check = 1; + break; + case HX509_SAN_TYPE_DNSNAME: + if ((ret = cmd_append(&cmd, " san_dnsname=", s, NULL))) + goto enomem; + do_check = 1; + break; + case HX509_SAN_TYPE_XMPP: + if ((ret = cmd_append(&cmd, " san_xmpp=", s, NULL))) + goto enomem; + do_check = 1; + break; + case HX509_SAN_TYPE_PKINIT: + if ((ret = cmd_append(&cmd, " san_pkinit=", s, NULL))) + goto enomem; + do_check = 1; + break; + case HX509_SAN_TYPE_MS_UPN: + if ((ret = cmd_append(&cmd, " san_ms_upn=", s, NULL))) + goto enomem; + do_check = 1; + break; + default: + if ((ret = hx509_request_reject_san(csr, i))) + goto out; + break; + } + frees(&s); + } + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret) + goto out; + + for (i = 0; ret == 0; i++) { + ret = hx509_request_get_eku(csr, i, &s); + if (ret) + break; + if ((ret = cmd_append(&cmd, " eku=", s, NULL))) + goto enomem; + do_check = 1; + frees(&s); + } + if (ret == HX509_NO_ITEM) + ret = 0; + if (ret) + goto out; + + ku = int2KeyUsage(0); + ku.digitalSignature = 1; + ku.nonRepudiation = 1; + hx509_request_authorize_ku(csr, ku); + + if (do_check) { + s = rk_strpoolcollect(cmd); + cmd = NULL; + if (s == NULL) + goto enomem; + if ((ret = call_svc(context, ipc, csr, s, piecemeal_check_ok))) + goto out; + } /* else there was nothing to check -> permit */ + + *result = TRUE; + ret = 0; + goto out; + +enomem: + ret = krb5_enomem(context); + goto out; + +out: + heim_ipc_free_context(ipc); + hx509_context_free(&hx509ctx); + if (cmd) + rk_strpoolfree(cmd); + free(princ); + free(s); + return ret; +} + +static KRB5_LIB_CALL krb5_error_code +ipc_csr_authorizer_init(krb5_context context, void **c) +{ + *c = NULL; + return 0; +} + +static KRB5_LIB_CALL void +ipc_csr_authorizer_fini(void *c) +{ +} + +static krb5plugin_csr_authorizer_ftable plug_desc = + { 1, ipc_csr_authorizer_init, ipc_csr_authorizer_fini, authorize }; + +static krb5plugin_csr_authorizer_ftable *plugs[] = { &plug_desc }; + +static uintptr_t +ipc_csr_authorizer_get_instance(const char *libname) +{ + if (strcmp(libname, "krb5") == 0) + return krb5_get_instance(libname); + if (strcmp(libname, "kdc") == 0) + return kdc_get_instance(libname); + if (strcmp(libname, "hx509") == 0) + return hx509_get_instance(libname); + return 0; +} + +krb5_plugin_load_ft kdc_csr_authorizer_plugin_load; + +krb5_error_code KRB5_CALLCONV +kdc_csr_authorizer_plugin_load(heim_pcontext context, + krb5_get_instance_func_t *get_instance, + size_t *num_plugins, + krb5_plugin_common_ftable_cp **plugins) +{ + *get_instance = ipc_csr_authorizer_get_instance; + *num_plugins = sizeof(plugs) / sizeof(plugs[0]); + *plugins = (krb5_plugin_common_ftable_cp *)plugs; + return 0; +} |