Path: blob/main/crypto/krb5/src/plugins/preauth/pkinit/pkinit_matching.c
34923 views
/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */1/*2* COPYRIGHT (C) 20073* THE REGENTS OF THE UNIVERSITY OF MICHIGAN4* ALL RIGHTS RESERVED5*6* Permission is granted to use, copy, create derivative works7* and redistribute this software and such derivative works8* for any purpose, so long as the name of The University of9* Michigan is not used in any advertising or publicity10* pertaining to the use of distribution of this software11* without specific, written prior authorization. If the12* above copyright notice or any other identification of the13* University of Michigan is included in any copy of any14* portion of this software, then the disclaimer below must15* also be included.16*17* THIS SOFTWARE IS PROVIDED AS IS, WITHOUT REPRESENTATION18* FROM THE UNIVERSITY OF MICHIGAN AS TO ITS FITNESS FOR ANY19* PURPOSE, AND WITHOUT WARRANTY BY THE UNIVERSITY OF20* MICHIGAN OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING21* WITHOUT LIMITATION THE IMPLIED WARRANTIES OF22* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE23* REGENTS OF THE UNIVERSITY OF MICHIGAN SHALL NOT BE LIABLE24* FOR ANY DAMAGES, INCLUDING SPECIAL, INDIRECT, INCIDENTAL, OR25* CONSEQUENTIAL DAMAGES, WITH RESPECT TO ANY CLAIM ARISING26* OUT OF OR IN CONNECTION WITH THE USE OF THE SOFTWARE, EVEN27* IF IT HAS BEEN OR IS HEREAFTER ADVISED OF THE POSSIBILITY OF28* SUCH DAMAGES.29*/3031#include <errno.h>32#include <string.h>33#include <stdio.h>34#include <stdlib.h>35#include "pkinit.h"36#include "k5-regex.h"3738typedef struct _pkinit_cert_info pkinit_cert_info;3940typedef enum {41kw_undefined = 0,42kw_subject = 1,43kw_issuer = 2,44kw_san = 3,45kw_eku = 4,46kw_ku = 547} keyword_type;4849static char *50keyword2string(unsigned int kw)51{52switch(kw) {53case kw_undefined: return "NONE"; break;54case kw_subject: return "SUBJECT"; break;55case kw_issuer: return "ISSUER"; break;56case kw_san: return "SAN"; break;57case kw_eku: return "EKU"; break;58case kw_ku: return "KU"; break;59default: return "INVALID"; break;60}61}62typedef enum {63relation_none = 0,64relation_and = 1,65relation_or = 266} relation_type;6768static char *69relation2string(unsigned int rel)70{71switch(rel) {72case relation_none: return "NONE"; break;73case relation_and: return "AND"; break;74case relation_or: return "OR"; break;75default: return "INVALID"; break;76}77}7879typedef enum {80kwvaltype_undefined = 0,81kwvaltype_regexp = 1,82kwvaltype_list = 283} kw_value_type;8485static char *86kwval2string(unsigned int kwval)87{88switch(kwval) {89case kwvaltype_undefined: return "NONE"; break;90case kwvaltype_regexp: return "REGEXP"; break;91case kwvaltype_list: return "LIST"; break;92default: return "INVALID"; break;93}94}9596struct keyword_desc {97const char *value;98size_t length;99keyword_type kwtype;100kw_value_type kwvaltype;101} matching_keywords[] = {102{ "<KU>", 4, kw_ku, kwvaltype_list },103{ "<EKU>", 5, kw_eku, kwvaltype_list },104{ "<SAN>", 5, kw_san, kwvaltype_regexp },105{ "<ISSUER>", 8, kw_issuer, kwvaltype_regexp },106{ "<SUBJECT>", 9, kw_subject, kwvaltype_regexp },107{ NULL, 0, kw_undefined, kwvaltype_undefined},108};109110struct ku_desc {111const char *value;112size_t length;113unsigned int bitval;114};115116struct ku_desc ku_keywords[] = {117{ "digitalSignature", 16, PKINIT_KU_DIGITALSIGNATURE },118{ "keyEncipherment", 15, PKINIT_KU_KEYENCIPHERMENT },119{ NULL, 0, 0 },120};121122struct ku_desc eku_keywords[] = {123{ "pkinit", 6, PKINIT_EKU_PKINIT },124{ "msScLogin", 9, PKINIT_EKU_MSSCLOGIN },125{ "clientAuth", 10, PKINIT_EKU_CLIENTAUTH },126{ "emailProtection", 15, PKINIT_EKU_EMAILPROTECTION },127{ NULL, 0, 0 },128};129130/* Rule component */131typedef struct _rule_component {132struct _rule_component *next;133keyword_type kw_type;134kw_value_type kwval_type;135regex_t regexp; /* Compiled regular expression */136char *regsrc; /* The regular expression source (for debugging) */137unsigned int ku_bits;138unsigned int eku_bits;139} rule_component;140141/* Set rule components */142typedef struct _rule_set {143relation_type relation;144int num_crs;145rule_component *crs;146} rule_set;147148static krb5_error_code149free_rule_component(krb5_context context,150rule_component *rc)151{152if (rc == NULL)153return 0;154155if (rc->kwval_type == kwvaltype_regexp) {156free(rc->regsrc);157regfree(&rc->regexp);158}159free(rc);160return 0;161}162163static krb5_error_code164free_rule_set(krb5_context context,165rule_set *rs)166{167rule_component *rc, *trc;168169if (rs == NULL)170return 0;171for (rc = rs->crs; rc != NULL;) {172trc = rc->next;173free_rule_component(context, rc);174rc = trc;175}176free(rs);177return 0;178}179180static krb5_error_code181parse_list_value(krb5_context context,182keyword_type type,183char *value,184rule_component *rc)185{186krb5_error_code retval;187char *comma;188struct ku_desc *ku = NULL;189int found;190size_t len;191unsigned int *bitptr;192193194if (value == NULL || value[0] == '\0') {195pkiDebug("%s: Missing or empty value for list keyword type %d\n",196__FUNCTION__, type);197retval = EINVAL;198goto out;199}200201if (type == kw_eku) {202bitptr = &rc->eku_bits;203} else if (type == kw_ku) {204bitptr = &rc->ku_bits;205} else {206pkiDebug("%s: Unknown list keyword type %d\n", __FUNCTION__, type);207retval = EINVAL;208goto out;209}210211do {212found = 0;213comma = strchr(value, ',');214if (comma != NULL)215len = comma - value;216else217len = strlen(value);218219if (type == kw_eku) {220ku = eku_keywords;221} else if (type == kw_ku) {222ku = ku_keywords;223}224225for (; ku->value != NULL; ku++) {226if (strncasecmp(value, ku->value, len) == 0) {227*bitptr |= ku->bitval;228found = 1;229pkiDebug("%s: Found value '%s', bitfield is now 0x%x\n",230__FUNCTION__, ku->value, *bitptr);231break;232}233}234if (found) {235value += ku->length;236if (*value == ',')237value += 1;238} else {239pkiDebug("%s: Urecognized value '%s'\n", __FUNCTION__, value);240retval = EINVAL;241goto out;242}243} while (found && *value != '\0');244245retval = 0;246out:247pkiDebug("%s: returning %d\n", __FUNCTION__, retval);248return retval;249}250251static krb5_error_code252parse_rule_component(krb5_context context,253const char **rule,254int *remaining,255rule_component **ret_rule)256{257krb5_error_code retval;258rule_component *rc = NULL;259keyword_type kw_type;260kw_value_type kwval_type;261char err_buf[128];262int ret;263struct keyword_desc *kw, *nextkw;264char *nk;265int found_next_kw = 0;266char *value = NULL;267size_t len;268269for (kw = matching_keywords; kw->value != NULL; kw++) {270if (strncmp(*rule, kw->value, kw->length) == 0) {271kw_type = kw->kwtype;272kwval_type = kw->kwvaltype;273*rule += kw->length;274*remaining -= kw->length;275break;276}277}278if (kw->value == NULL) {279pkiDebug("%s: Missing or invalid keyword in rule '%s'\n",280__FUNCTION__, *rule);281retval = ENOENT;282goto out;283}284285pkiDebug("%s: found keyword '%s'\n", __FUNCTION__, kw->value);286287rc = calloc(1, sizeof(*rc));288if (rc == NULL) {289retval = ENOMEM;290goto out;291}292rc->next = NULL;293rc->kw_type = kw_type;294rc->kwval_type = kwval_type;295296/*297* Before processing the value for this keyword,298* (compiling the regular expression or processing the list)299* we need to find the end of it. That means parsing for the300* beginning of the next keyword (or the end of the rule).301*/302nk = strchr(*rule, '<');303while (nk != NULL) {304/* Possibly another keyword, check it out */305for (nextkw = matching_keywords; nextkw->value != NULL; nextkw++) {306if (strncmp(nk, nextkw->value, nextkw->length) == 0) {307/* Found a keyword, nk points to the beginning */308found_next_kw = 1;309break; /* Need to break out of the while! */310}311}312if (!found_next_kw)313nk = strchr(nk+1, '<'); /* keep looking */314else315break;316}317318if (nk != NULL && found_next_kw)319len = (nk - *rule);320else321len = (*remaining);322323if (len == 0) {324pkiDebug("%s: Missing value for keyword '%s'\n",325__FUNCTION__, kw->value);326retval = EINVAL;327goto out;328}329330value = calloc(1, len+1);331if (value == NULL) {332retval = ENOMEM;333goto out;334}335memcpy(value, *rule, len);336*remaining -= len;337*rule += len;338pkiDebug("%s: found value '%s'\n", __FUNCTION__, value);339340if (kw->kwvaltype == kwvaltype_regexp) {341ret = regcomp(&rc->regexp, value, REG_EXTENDED);342if (ret) {343regerror(ret, &rc->regexp, err_buf, sizeof(err_buf));344pkiDebug("%s: Error compiling reg-exp '%s': %s\n",345__FUNCTION__, value, err_buf);346retval = ret;347goto out;348}349rc->regsrc = strdup(value);350if (rc->regsrc == NULL) {351retval = ENOMEM;352goto out;353}354} else if (kw->kwvaltype == kwvaltype_list) {355retval = parse_list_value(context, rc->kw_type, value, rc);356if (retval) {357pkiDebug("%s: Error %d, parsing list values for keyword %s\n",358__FUNCTION__, retval, kw->value);359goto out;360}361}362363*ret_rule = rc;364retval = 0;365out:366free(value);367if (retval && rc != NULL)368free_rule_component(context, rc);369pkiDebug("%s: returning %d\n", __FUNCTION__, retval);370return retval;371}372373static krb5_error_code374parse_rule_set(krb5_context context,375const char *rule_in,376rule_set **out_rs)377{378const char *rule;379int remaining;380krb5_error_code ret, retval;381rule_component *rc = NULL, *trc;382rule_set *rs;383384385if (rule_in == NULL)386return EINVAL;387rule = rule_in;388remaining = strlen(rule);389390rs = calloc(1, sizeof(*rs));391if (rs == NULL) {392retval = ENOMEM;393goto cleanup;394}395396rs->relation = relation_none;397if (remaining > 1) {398if (rule[0] == '&' && rule[1] == '&') {399rs->relation = relation_and;400rule += 2;401remaining -= 2;402} else if (rule_in[0] == '|' && rule_in[1] == '|') {403rs->relation = relation_or;404rule +=2;405remaining -= 2;406}407}408rs->num_crs = 0;409while (remaining > 0) {410if (rs->relation == relation_none && rs->num_crs > 0) {411pkiDebug("%s: Assuming AND relation for multiple components in rule '%s'\n",412__FUNCTION__, rule_in);413rs->relation = relation_and;414}415ret = parse_rule_component(context, &rule, &remaining, &rc);416if (ret) {417retval = ret;418goto cleanup;419}420pkiDebug("%s: After parse_rule_component, remaining %d, rule '%s'\n",421__FUNCTION__, remaining, rule);422rs->num_crs++;423424/*425* Chain the new component on the end (order matters since426* we can short-circuit an OR or an AND relation if an427* earlier check passes428*/429for (trc = rs->crs; trc != NULL && trc->next != NULL; trc = trc->next);430if (trc == NULL)431rs->crs = rc;432else {433trc->next = rc;434}435}436437*out_rs = rs;438439retval = 0;440cleanup:441if (retval && rs != NULL) {442free_rule_set(context, rs);443}444pkiDebug("%s: returning %d\n", __FUNCTION__, retval);445return retval;446}447448static int449regexp_match(krb5_context context, rule_component *rc, char *value, int idx)450{451int code;452453code = regexec(&rc->regexp, value, 0, NULL, 0);454455if (code == 0) {456TRACE_PKINIT_REGEXP_MATCH(context, keyword2string(rc->kw_type),457rc->regsrc, value, idx);458} else {459TRACE_PKINIT_REGEXP_NOMATCH(context, keyword2string(rc->kw_type),460rc->regsrc, value, idx);461}462463return (code == 0 ? 1: 0);464}465466static int467component_match(krb5_context context, rule_component *rc,468pkinit_cert_matching_data *md, int idx)469{470int match = 0;471int i;472char *princ_string;473474switch (rc->kwval_type) {475case kwvaltype_regexp:476switch (rc->kw_type) {477case kw_subject:478match = regexp_match(context, rc, md->subject_dn, idx);479break;480case kw_issuer:481match = regexp_match(context, rc, md->issuer_dn, idx);482break;483case kw_san:484for (i = 0; md->sans != NULL && md->sans[i] != NULL; i++) {485krb5_unparse_name(context, md->sans[i], &princ_string);486match = regexp_match(context, rc, princ_string, idx);487krb5_free_unparsed_name(context, princ_string);488if (match)489break;490}491for (i = 0; md->upns != NULL && md->upns[i] != NULL; i++) {492match = regexp_match(context, rc, md->upns[i], idx);493if (match)494break;495}496break;497default:498pkiDebug("%s: keyword %s, keyword value %s mismatch\n",499__FUNCTION__, keyword2string(rc->kw_type),500kwval2string(kwvaltype_regexp));501break;502}503break;504case kwvaltype_list:505switch(rc->kw_type) {506case kw_eku:507pkiDebug("%s: checking %s: rule 0x%08x, cert 0x%08x\n",508__FUNCTION__, keyword2string(rc->kw_type),509rc->eku_bits, md->eku_bits);510if ((rc->eku_bits & md->eku_bits) == rc->eku_bits)511match = 1;512break;513case kw_ku:514pkiDebug("%s: checking %s: rule 0x%08x, cert 0x%08x\n",515__FUNCTION__, keyword2string(rc->kw_type),516rc->ku_bits, md->ku_bits);517if ((rc->ku_bits & md->ku_bits) == rc->ku_bits)518match = 1;519break;520default:521pkiDebug("%s: keyword %s, keyword value %s mismatch\n",522__FUNCTION__, keyword2string(rc->kw_type),523kwval2string(kwvaltype_regexp));524break;525}526break;527default:528pkiDebug("%s: unknown keyword value type %d\n",529__FUNCTION__, rc->kwval_type);530break;531}532pkiDebug("%s: returning match = %d\n", __FUNCTION__, match);533return match;534}535/*536* Returns match_found == 1 only if exactly one certificate matches537* the given rule538*/539static krb5_error_code540check_all_certs(krb5_context context,541pkinit_plg_crypto_context plg_cryptoctx,542pkinit_req_crypto_context req_cryptoctx,543pkinit_identity_crypto_context id_cryptoctx,544krb5_principal princ,545rule_set *rs, /* rule to check */546pkinit_cert_matching_data **matchdata,547int *match_found,548size_t *match_index)549{550krb5_error_code retval;551pkinit_cert_matching_data *md;552int i;553int comp_match = 0;554int total_cert_matches = 0;555rule_component *rc;556int certs_checked = 0;557size_t save_index = 0;558559if (match_found == NULL || match_index == NULL)560return EINVAL;561562*match_index = 0;563*match_found = 0;564565pkiDebug("%s: matching rule relation is %s with %d components\n",566__FUNCTION__, relation2string(rs->relation), rs->num_crs);567568/*569* Loop through all the certs available and count570* how many match the rule571*/572for (i = 0, md = matchdata[i]; md != NULL; md = matchdata[++i]) {573pkiDebug("%s: subject: '%s'\n", __FUNCTION__, md->subject_dn);574certs_checked++;575for (rc = rs->crs; rc != NULL; rc = rc->next) {576comp_match = component_match(context, rc, md, i);577if (comp_match) {578pkiDebug("%s: match for keyword type %s\n",579__FUNCTION__, keyword2string(rc->kw_type));580}581if (comp_match && rs->relation == relation_or) {582pkiDebug("%s: cert matches rule (OR relation)\n",583__FUNCTION__);584total_cert_matches++;585save_index = i;586goto nextcert;587}588if (!comp_match && rs->relation == relation_and) {589pkiDebug("%s: cert does not match rule (AND relation)\n",590__FUNCTION__);591goto nextcert;592}593}594if (rc == NULL && comp_match) {595pkiDebug("%s: cert matches rule (AND relation)\n", __FUNCTION__);596total_cert_matches++;597save_index = i;598}599nextcert:600continue;601}602TRACE_PKINIT_CERT_NUM_MATCHING(context, certs_checked, total_cert_matches);603if (total_cert_matches == 1) {604*match_found = 1;605*match_index = save_index;606}607608retval = 0;609610pkiDebug("%s: returning %d, match_found %d\n",611__FUNCTION__, retval, *match_found);612return retval;613}614615krb5_error_code616pkinit_cert_matching(krb5_context context,617pkinit_plg_crypto_context plg_cryptoctx,618pkinit_req_crypto_context req_cryptoctx,619pkinit_identity_crypto_context id_cryptoctx,620krb5_principal princ)621{622623krb5_error_code retval = KRB5KDC_ERR_PREAUTH_FAILED;624int x;625char **rules = NULL;626rule_set *rs = NULL;627int match_found = 0;628pkinit_cert_matching_data **matchdata = NULL;629size_t match_index = 0;630631/* If no matching rules, select the default cert and we're done */632pkinit_libdefault_strings(context, krb5_princ_realm(context, princ),633KRB5_CONF_PKINIT_CERT_MATCH, &rules);634if (rules == NULL) {635pkiDebug("%s: no matching rules found in config file\n", __FUNCTION__);636retval = crypto_cert_select_default(context, plg_cryptoctx,637req_cryptoctx, id_cryptoctx);638goto cleanup;639}640641/* parse each rule line one at a time and check all the certs against it */642for (x = 0; rules[x] != NULL; x++) {643TRACE_PKINIT_CERT_RULE(context, rules[x]);644645/* Free rules from previous time through... */646if (rs != NULL) {647free_rule_set(context, rs);648rs = NULL;649}650retval = parse_rule_set(context, rules[x], &rs);651if (retval) {652if (retval == EINVAL) {653TRACE_PKINIT_CERT_RULE_INVALID(context, rules[x]);654continue;655}656goto cleanup;657}658659/*660* Optimize so that we do not get cert info unless we have661* valid rules to check. Once obtained, keep it around662* until we are done.663*/664if (matchdata == NULL) {665retval = crypto_cert_get_matching_data(context, plg_cryptoctx,666req_cryptoctx, id_cryptoctx,667&matchdata);668if (retval || matchdata == NULL) {669pkiDebug("%s: Error %d obtaining certificate information\n",670__FUNCTION__, retval);671retval = ENOENT;672goto cleanup;673}674}675676retval = check_all_certs(context, plg_cryptoctx, req_cryptoctx,677id_cryptoctx, princ, rs, matchdata,678&match_found, &match_index);679if (retval) {680pkiDebug("%s: Error %d, checking certs against rule '%s'\n",681__FUNCTION__, retval, rules[x]);682goto cleanup;683}684if (match_found) {685pkiDebug("%s: We have an exact match with rule '%s'\n",686__FUNCTION__, rules[x]);687break;688}689}690691if (match_found) {692pkiDebug("%s: Selecting the matching cert!\n", __FUNCTION__);693retval = crypto_cert_select(context, id_cryptoctx, match_index);694if (retval) {695pkiDebug("%s: crypto_cert_select error %d, %s\n",696__FUNCTION__, retval, error_message(retval));697goto cleanup;698}699} else {700TRACE_PKINIT_NO_MATCHING_CERT(context);701retval = ENOENT; /* XXX */702goto cleanup;703}704705retval = 0;706707cleanup:708profile_free_list(rules);709free_rule_set(context, rs);710crypto_cert_free_matching_data_list(context, matchdata);711return retval;712}713714krb5_error_code715pkinit_client_cert_match(krb5_context context,716pkinit_plg_crypto_context plgctx,717pkinit_req_crypto_context reqctx,718const char *match_rule,719krb5_boolean *matched)720{721krb5_error_code ret;722pkinit_cert_matching_data *md = NULL;723rule_component *rc = NULL;724int comp_match = 0;725rule_set *rs = NULL;726727*matched = FALSE;728ret = parse_rule_set(context, match_rule, &rs);729if (ret)730goto cleanup;731732ret = crypto_req_cert_matching_data(context, plgctx, reqctx, &md);733if (ret)734goto cleanup;735736for (rc = rs->crs; rc != NULL; rc = rc->next) {737comp_match = component_match(context, rc, md, 0);738if ((comp_match && rs->relation == relation_or) ||739(!comp_match && rs->relation == relation_and)) {740break;741}742}743*matched = comp_match;744745cleanup:746free_rule_set(context, rs);747crypto_cert_free_matching_data(context, md);748return ret;749}750751752