const char *redir;
const char *method;
size_t post_data_size;
+ size_t san_idx; /* For /get-tgts */
enum k5_creds_kind cckind;
char *pkix_store;
char *tgts_filename;
ret = kdc_authorize_csr(r->context, "get-tgt", r->req, p);
krb5_free_principal(r->context, p);
hx509_request_free(&r->req);
+ r->req = NULL;
if (ret)
return bad_403(r, ret, "Not authorized to requested TGT");
return ret;
const char *val)
{
struct bx509_request_desc *r = d;
- heim_mhd_result res = MHD_YES;
+ hx509_san_type san_type;
krb5_error_code ret;
+ size_t san_idx = r->san_idx++;
+ const char *save_for_cname = r->for_cname;
+ char *s = NULL;
- if (strcmp(key, "cname") == 0 && val) {
- /* Handled upstairs */
+ /* We expect only cname=principal q-params here */
+ if (strcmp(key, "cname") != 0 || val == NULL)
+ return MHD_YES;
+
+ /*
+ * We expect the `san_idx'th SAN in the `r->req' request checked by
+ * kdc_authorize_csr() to be the same as this cname. This happens
+ * naturally because we add these SANs to `r->req' in the same order as we
+ * visit them here (unless our HTTP library somehow went crazy).
+ *
+ * Still, we check that it's the same SAN.
+ */
+ ret = hx509_request_get_san(r->req, san_idx, &san_type, &s);
+ if (ret == HX509_NO_ITEM ||
+ san_type != HX509_SAN_TYPE_PKINIT ||
+ strcmp(s, val) != 0) {
+ /*
+ * If the cname and SAN don't match, it's some weird internal error
+ * (can't happen).
+ */
+ krb5_set_error_message(r->context, r->error_code = EACCES,
+ "PKINIT SAN not granted: %s (internal error)",
+ val);
+ ret = EACCES;
+ }
+
+ /*
+ * We're going to pretend to be this SAN for the purpose of acquring a TGT
+ * for it. So we "push" `r->for_cname'.
+ */
+ if (ret == 0)
r->for_cname = val;
+
+ /*
+ * Our authorizer supports partial authorization where the whole request is
+ * rejected but some features of it are permitted.
+ *
+ * (In most end-points we don't want partial authorization, but in
+ * /get-tgts we very much do.)
+ */
+ if (ret == 0 && !hx509_request_san_authorized_p(r->req, san_idx)) {
+ heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
+ "REJECT_krb5PrincipalName", "%s", val);
+ krb5_set_error_message(r->context, r->error_code = EACCES,
+ "PKINIT SAN denied: %s", val);
+ ret = EACCES;
+ }
+ if (ret == 0) {
+ heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
+ "ACCEPT_krb5PrincipalName", "%s", val);
ret = k5_get_creds(r, K5_CREDS_EPHEMERAL);
- res = get_tgts_accumulate_ccache(r, ret);
- } else {
- /* Handled upstairs */
+ if (ret == 0)
+ heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
+ "ISSUE_krb5PrincipalName", "%s", val);
}
- return res;
+
+ /*
+ * If ret == 0 this will gather the TGT we acquired, else it will acquire
+ * the error we got.
+ */
+ ret = get_tgts_accumulate_ccache(r, ret);
+
+ /* Now we "pop" `r->for_cname' */
+ r->for_cname = save_for_cname;
+
+ hx509_xfree(s);
+ return MHD_YES;
}
/*
ret = r->error_code;
}
if (ret == 0) {
- /* Authorize requested client principal names (calls bad_req()) */
+ /*
+ * Check authorization of the authenticated client to the requested
+ * client principal names (calls bad_req()).
+ */
r->error_code = 0;
res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
get_tgts_param_authorize_cb, r);
ret = r->error_code;
if (ret == 0) {
+ /* Use the same configuration as /get-tgt (or should we?) */
ret = kdc_authorize_csr(r->context, "get-tgt", r->req, p);
+
+ /*
+ * We tolerate EACCES because we support partial approval.
+ *
+ * (KRB5_PLUGIN_NO_HANDLE means no plugin handled the authorization
+ * check.)
+ */
+ if (ret == EACCES || ret == KRB5_PLUGIN_NO_HANDLE)
+ ret = 0;
if (ret) {
krb5_free_principal(r->context, p);
return bad_403(r, ret, "Permission denied");
}
}
- hx509_request_free(&r->req);
}
if (ret == 0) {
- /* get_tgts_param_execute_cb() calls bad_req() */
+ /*
+ * Get the actual TGTs that were authorized.
+ *
+ * get_tgts_param_execute_cb() calls bad_req()
+ */
r->error_code = 0;
res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
get_tgts_param_execute_cb, r);
ret = r->error_code;
}
krb5_free_principal(r->context, p);
+ hx509_request_free(&r->req);
+ r->req = NULL;
/*
* get_tgts_param_execute_cb() will write its JSON response to the file
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
-call_svc(krb5_context context, heim_ipc ipc, const char *cmd)
+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.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 = 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) {
- free(resp.data);
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) {
- 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;
+ return EACCES;
}
- krb5_set_error_message(context, ret, "CSR authorizer failed %s: %.*s",
- cmd, resp.length < INT_MAX ? (int)resp.length : 0,
- 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
char *princ = NULL;
char *s = NULL;
int do_check = 0;
+ int piecemeal_check_ok = 1;
if ((svc = krb5_config_get_string(context, NULL, app ? app : "kdc",
"ipc_csr_authorizer", "service", NULL))
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)))
if ((s = rk_strpoolcollect(cmd)) == NULL)
goto enomem;
cmd = NULL;
- if ((ret = call_svc(context, ipc, s)))
+ if ((ret = call_svc(context, ipc, csr, s, piecemeal_check_ok)))
goto out;
- } /* else -> permit */
-
- if ((ret = mark_authorized(csr)))
- goto out;
+ } /* else there was nothing to check -> permit */
*result = TRUE;
ret = 0;