#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <regex.h>
#include <krb5.h>
#include "pkinit.h"
typedef struct _pkinit_cert_info pkinit_cert_info;
typedef enum {
kw_undefined = 0,
kw_subject = 1,
kw_issuer = 2,
kw_san = 3,
kw_eku = 4,
kw_ku = 5
} keyword_type;
static char *
keyword2string(unsigned int kw)
{
switch(kw) {
case kw_undefined: return "NONE"; break;
case kw_subject: return "SUBJECT"; break;
case kw_issuer: return "ISSUER"; break;
case kw_san: return "SAN"; break;
case kw_eku: return "EKU"; break;
case kw_ku: return "KU"; break;
default: return "INVALID"; break;
}
}
typedef enum {
relation_none = 0,
relation_and = 1,
relation_or = 2
} relation_type;
static char *
relation2string(unsigned int rel)
{
switch(rel) {
case relation_none: return "NONE"; break;
case relation_and: return "AND"; break;
case relation_or: return "OR"; break;
default: return "INVALID"; break;
}
}
typedef enum {
kwvaltype_undefined = 0,
kwvaltype_regexp = 1,
kwvaltype_list = 2
} kw_value_type;
static char *
kwval2string(unsigned int kwval)
{
switch(kwval) {
case kwvaltype_undefined: return "NONE"; break;
case kwvaltype_regexp: return "REGEXP"; break;
case kwvaltype_list: return "LIST"; break;
default: return "INVALID"; break;
}
}
struct keyword_desc {
const char *value;
size_t length;
keyword_type kwtype;
kw_value_type kwvaltype;
} matching_keywords[] = {
{ "<KU>", 4, kw_ku, kwvaltype_list },
{ "<EKU>", 5, kw_eku, kwvaltype_list },
{ "<SAN>", 5, kw_san, kwvaltype_regexp },
{ "<ISSUER>", 8, kw_issuer, kwvaltype_regexp },
{ "<SUBJECT>", 9, kw_subject, kwvaltype_regexp },
{ NULL, 0, kw_undefined, kwvaltype_undefined},
};
struct ku_desc {
const char *value;
size_t length;
unsigned int bitval;
};
struct ku_desc ku_keywords[] = {
{ "digitalSignature", 16, PKINIT_KU_DIGITALSIGNATURE },
{ "keyEncipherment", 15, PKINIT_KU_KEYENCIPHERMENT },
{ NULL, 0, 0 },
};
struct ku_desc eku_keywords[] = {
{ "pkinit", 6, PKINIT_EKU_PKINIT },
{ "msScLogin", 9, PKINIT_EKU_MSSCLOGIN },
{ "clientAuth", 10, PKINIT_EKU_CLIENTAUTH },
{ "emailProtection", 15, PKINIT_EKU_EMAILPROTECTION },
{ NULL, 0, 0 },
};
typedef struct _rule_component {
struct _rule_component *next;
keyword_type kw_type;
kw_value_type kwval_type;
regex_t regexp;
char *regsrc;
unsigned int ku_bits;
unsigned int eku_bits;
} rule_component;
typedef struct _rule_set {
relation_type relation;
int num_crs;
rule_component *crs;
} rule_set;
static krb5_error_code
free_rule_component(krb5_context context,
rule_component *rc)
{
if (rc == NULL)
return 0;
if (rc->kwval_type == kwvaltype_regexp) {
if (rc->regsrc)
free(rc->regsrc);
regfree(&rc->regexp);
}
free(rc);
return 0;
}
static krb5_error_code
free_rule_set(krb5_context context,
rule_set *rs)
{
rule_component *rc, *trc;
if (rs == NULL)
return 0;
for (rc = rs->crs; rc != NULL;) {
trc = rc->next;
free_rule_component(context, rc);
rc = trc;
}
free(rs);
return 0;
}
static krb5_error_code
parse_list_value(krb5_context context,
keyword_type type,
char *value,
rule_component *rc)
{
krb5_error_code retval;
char *comma;
struct ku_desc *ku = NULL;
int found;
size_t len;
unsigned int *bitptr;
if (value == NULL || value[0] == '\0') {
pkiDebug("%s: Missing or empty value for list keyword type %d\n",
__FUNCTION__, type);
retval = EINVAL;
goto out;
}
if (type == kw_eku) {
bitptr = &rc->eku_bits;
} else if (type == kw_ku) {
bitptr = &rc->ku_bits;
} else {
pkiDebug("%s: Unknown list keyword type %d\n", __FUNCTION__, type);
retval = EINVAL;
goto out;
}
do {
found = 0;
comma = strchr(value, ',');
if (comma != NULL)
len = comma - value;
else
len = strlen(value);
if (type == kw_eku) {
ku = eku_keywords;
} else if (type == kw_ku) {
ku = ku_keywords;
}
for (; ku->value != NULL; ku++) {
if (strncasecmp(value, ku->value, len) == 0) {
*bitptr |= ku->bitval;
found = 1;
pkiDebug("%s: Found value '%s', bitfield is now 0x%x\n",
__FUNCTION__, ku->value, *bitptr);
break;
}
}
if (found) {
value += ku->length;
if (*value == ',')
value += 1;
} else {
pkiDebug("%s: Urecognized value '%s'\n", __FUNCTION__, value);
retval = EINVAL;
goto out;
}
} while (found && *value != '\0');
retval = 0;
out:
pkiDebug("%s: returning %d\n", __FUNCTION__, retval);
return retval;
}
static krb5_error_code
parse_rule_component(krb5_context context,
const char **rule,
int *remaining,
rule_component **ret_rule)
{
krb5_error_code retval;
rule_component *rc = NULL;
keyword_type kw_type;
kw_value_type kwval_type;
char err_buf[128];
int ret;
struct keyword_desc *kw, *nextkw;
char *nk;
int found_next_kw = 0;
char *value = NULL;
size_t len;
for (kw = matching_keywords; kw->value != NULL; kw++) {
if (strncmp(*rule, kw->value, kw->length) == 0) {
kw_type = kw->kwtype;
kwval_type = kw->kwvaltype;
*rule += kw->length;
*remaining -= kw->length;
break;
}
}
if (kw->value == NULL) {
pkiDebug("%s: Missing or invalid keyword in rule '%s'\n",
__FUNCTION__, *rule);
retval = ENOENT;
goto out;
}
pkiDebug("%s: found keyword '%s'\n", __FUNCTION__, kw->value);
rc = calloc(1, sizeof(*rc));
if (rc == NULL) {
retval = ENOMEM;
goto out;
}
rc->next = NULL;
rc->kw_type = kw_type;
rc->kwval_type = kwval_type;
nk = strchr(*rule, '<');
while (nk != NULL) {
for (nextkw = matching_keywords; nextkw->value != NULL; nextkw++) {
if (strncmp(nk, nextkw->value, nextkw->length) == 0) {
found_next_kw = 1;
break;
}
}
if (!found_next_kw)
nk = strchr(nk+1, '<');
else
break;
}
if (nk != NULL && found_next_kw)
len = (nk - *rule);
else
len = (*remaining);
if (len == 0) {
pkiDebug("%s: Missing value for keyword '%s'\n",
__FUNCTION__, kw->value);
retval = EINVAL;
goto out;
}
value = calloc(1, len+1);
if (value == NULL) {
retval = ENOMEM;
goto out;
}
memcpy(value, *rule, len);
*remaining -= len;
*rule += len;
pkiDebug("%s: found value '%s'\n", __FUNCTION__, value);
if (kw->kwvaltype == kwvaltype_regexp) {
ret = regcomp(&rc->regexp, value, REG_EXTENDED);
if (ret) {
regerror(ret, &rc->regexp, err_buf, sizeof(err_buf));
pkiDebug("%s: Error compiling reg-exp '%s': %s\n",
__FUNCTION__, value, err_buf);
retval = ret;
goto out;
}
rc->regsrc = strdup(value);
if (rc->regsrc == NULL) {
retval = ENOMEM;
goto out;
}
} else if (kw->kwvaltype == kwvaltype_list) {
retval = parse_list_value(context, rc->kw_type, value, rc);
if (retval) {
pkiDebug("%s: Error %d, parsing list values for keyword %s\n",
__FUNCTION__, retval, kw->value);
goto out;
}
}
*ret_rule = rc;
retval = 0;
out:
if (value != NULL)
free(value);
if (retval && rc != NULL)
free_rule_component(context, rc);
pkiDebug("%s: returning %d\n", __FUNCTION__, retval);
return retval;
}
static krb5_error_code
parse_rule_set(krb5_context context,
const char *rule_in,
rule_set **out_rs)
{
const char *rule;
int remaining, totlen;
krb5_error_code ret, retval;
rule_component *rc = NULL, *trc;
rule_set *rs;
if (rule_in == NULL)
return EINVAL;
rule = rule_in;
totlen = remaining = strlen(rule);
rs = calloc(1, sizeof(*rs));
if (rs == NULL) {
retval = ENOMEM;
goto cleanup;
}
rs->relation = relation_none;
if (remaining > 1) {
if (rule[0] == '&' && rule[1] == '&') {
rs->relation = relation_and;
rule += 2;
remaining -= 2;
} else if (rule_in[0] == '|' && rule_in[1] == '|') {
rs->relation = relation_or;
rule +=2;
remaining -= 2;
}
}
rs->num_crs = 0;
while (remaining > 0) {
if (rs->relation == relation_none && rs->num_crs > 1) {
pkiDebug("%s: Assuming AND relation for multiple components in rule '%s'\n",
__FUNCTION__, rule_in);
rs->relation = relation_and;
}
ret = parse_rule_component(context, &rule, &remaining, &rc);
if (ret) {
retval = ret;
goto cleanup;
}
pkiDebug("%s: After parse_rule_component, remaining %d, rule '%s'\n",
__FUNCTION__, remaining, rule);
rs->num_crs++;
for (trc = rs->crs; trc != NULL && trc->next != NULL; trc = trc->next);
if (trc == NULL)
rs->crs = rc;
else {
trc->next = rc;
}
}
*out_rs = rs;
retval = 0;
cleanup:
if (retval && rs != NULL) {
free_rule_set(context, rs);
}
pkiDebug("%s: returning %d\n", __FUNCTION__, retval);
return retval;
}
static int
regexp_match(krb5_context context, rule_component *rc, char *value)
{
int code;
pkiDebug("%s: checking %s rule '%s' with value '%s'\n",
__FUNCTION__, keyword2string(rc->kw_type), rc->regsrc, value);
code = regexec(&rc->regexp, value, 0, NULL, 0);
pkiDebug("%s: the result is%s a match\n", __FUNCTION__,
code == REG_NOMATCH ? " NOT" : "");
return (code == 0 ? 1: 0);
}
static int
component_match(krb5_context context,
rule_component *rc,
pkinit_cert_matching_data *md)
{
int match = 0;
int i;
krb5_principal p;
char *princ_string;
switch (rc->kwval_type) {
case kwvaltype_regexp:
switch (rc->kw_type) {
case kw_subject:
match = regexp_match(context, rc, md->subject_dn);
break;
case kw_issuer:
match = regexp_match(context, rc, md->issuer_dn);
break;
case kw_san:
if (md->sans == NULL)
break;
for (i = 0, p = md->sans[i]; p != NULL; p = md->sans[++i]) {
krb5_unparse_name(context, p, &princ_string);
match = regexp_match(context, rc, princ_string);
krb5_free_unparsed_name(context, princ_string);
if (match)
break;
}
break;
default:
pkiDebug("%s: keyword %s, keyword value %s mismatch\n",
__FUNCTION__, keyword2string(rc->kw_type),
kwval2string(kwvaltype_regexp));
break;
}
break;
case kwvaltype_list:
switch(rc->kw_type) {
case kw_eku:
pkiDebug("%s: checking %s: rule 0x%08x, cert 0x%08x\n",
__FUNCTION__, keyword2string(rc->kw_type),
rc->eku_bits, md->eku_bits);
if ((rc->eku_bits & md->eku_bits) == rc->eku_bits)
match = 1;
break;
case kw_ku:
pkiDebug("%s: checking %s: rule 0x%08x, cert 0x%08x\n",
__FUNCTION__, keyword2string(rc->kw_type),
rc->ku_bits, md->ku_bits);
if ((rc->ku_bits & md->ku_bits) == rc->ku_bits)
match = 1;
break;
default:
pkiDebug("%s: keyword %s, keyword value %s mismatch\n",
__FUNCTION__, keyword2string(rc->kw_type),
kwval2string(kwvaltype_regexp));
break;
}
break;
default:
pkiDebug("%s: unknown keyword value type %d\n",
__FUNCTION__, rc->kwval_type);
break;
}
pkiDebug("%s: returning match = %d\n", __FUNCTION__, match);
return match;
}
static krb5_error_code
check_all_certs(krb5_context context,
pkinit_plg_crypto_context plg_cryptoctx,
pkinit_req_crypto_context req_cryptoctx,
pkinit_identity_crypto_context id_cryptoctx,
krb5_principal princ,
rule_set *rs,
pkinit_cert_matching_data **matchdata,
int *match_found,
pkinit_cert_matching_data **matching_cert)
{
krb5_error_code retval;
pkinit_cert_matching_data *md;
int i;
int comp_match = 0;
int total_cert_matches = 0;
rule_component *rc;
int certs_checked = 0;
pkinit_cert_matching_data *save_match = NULL;
if (match_found == NULL || matching_cert == NULL)
return EINVAL;
*matching_cert = NULL;
*match_found = 0;
pkiDebug("%s: matching rule relation is %s with %d components\n",
__FUNCTION__, relation2string(rs->relation), rs->num_crs);
for (i = 0, md = matchdata[i]; md != NULL; md = matchdata[++i]) {
pkiDebug("%s: subject: '%s'\n", __FUNCTION__, md->subject_dn);
#if 0
pkiDebug("%s: issuer: '%s'\n", __FUNCTION__, md->subject_dn);
for (j = 0, p = md->sans[j]; p != NULL; p = md->sans[++j]) {
char *san_string;
krb5_unparse_name(context, p, &san_string);
pkiDebug("%s: san: '%s'\n", __FUNCTION__, san_string);
krb5_free_unparsed_name(context, san_string);
}
#endif
certs_checked++;
for (rc = rs->crs; rc != NULL; rc = rc->next) {
comp_match = component_match(context, rc, md);
if (comp_match) {
pkiDebug("%s: match for keyword type %s\n",
__FUNCTION__, keyword2string(rc->kw_type));
}
if (comp_match && rs->relation == relation_or) {
pkiDebug("%s: cert matches rule (OR relation)\n",
__FUNCTION__);
total_cert_matches++;
save_match = md;
goto nextcert;
}
if (!comp_match && rs->relation == relation_and) {
pkiDebug("%s: cert does not match rule (AND relation)\n",
__FUNCTION__);
goto nextcert;
}
}
if (rc == NULL && comp_match) {
pkiDebug("%s: cert matches rule (AND relation)\n", __FUNCTION__);
total_cert_matches++;
save_match = md;
}
nextcert:
continue;
}
pkiDebug("%s: After checking %d certs, we found %d matches\n",
__FUNCTION__, certs_checked, total_cert_matches);
if (total_cert_matches == 1) {
*match_found = 1;
*matching_cert = save_match;
}
retval = 0;
pkiDebug("%s: returning %d, match_found %d\n",
__FUNCTION__, retval, *match_found);
return retval;
}
static krb5_error_code
free_all_cert_matching_data(krb5_context context,
pkinit_cert_matching_data **matchdata)
{
krb5_error_code retval;
pkinit_cert_matching_data *md;
int i;
if (matchdata == NULL)
return EINVAL;
for (i = 0, md = matchdata[i]; md != NULL; md = matchdata[++i]) {
pkinit_cert_handle ch = md->ch;
retval = crypto_cert_free_matching_data(context, md);
if (retval) {
pkiDebug("%s: crypto_cert_free_matching_data error %d, %s\n",
__FUNCTION__, retval, error_message(retval));
goto cleanup;
}
retval = crypto_cert_release(context, ch);
if (retval) {
pkiDebug("%s: crypto_cert_release error %d, %s\n",
__FUNCTION__, retval, error_message(retval));
goto cleanup;
}
}
free(matchdata);
retval = 0;
cleanup:
return retval;
}
static krb5_error_code
obtain_all_cert_matching_data(krb5_context context,
pkinit_plg_crypto_context plg_cryptoctx,
pkinit_req_crypto_context req_cryptoctx,
pkinit_identity_crypto_context id_cryptoctx,
pkinit_cert_matching_data ***all_matching_data)
{
krb5_error_code retval;
int i, cert_count;
pkinit_cert_iter_handle ih = NULL;
pkinit_cert_handle ch;
pkinit_cert_matching_data **matchdata = NULL;
retval = crypto_cert_get_count(context, plg_cryptoctx, req_cryptoctx,
id_cryptoctx, &cert_count);
if (retval) {
pkiDebug("%s: crypto_cert_get_count error %d, %s\n",
__FUNCTION__, retval, error_message(retval));
goto cleanup;
}
pkiDebug("%s: crypto_cert_get_count says there are %d certs\n",
__FUNCTION__, cert_count);
matchdata = calloc((size_t)cert_count + 1, sizeof(*matchdata));
if (matchdata == NULL)
return ENOMEM;
retval = crypto_cert_iteration_begin(context, plg_cryptoctx, req_cryptoctx,
id_cryptoctx, &ih);
if (retval) {
pkiDebug("%s: crypto_cert_iteration_begin returned %d, %s\n",
__FUNCTION__, retval, error_message(retval));
goto cleanup;
}
for (i = 0; i < cert_count; i++) {
retval = crypto_cert_iteration_next(context, ih, &ch);
if (retval) {
if (retval == PKINIT_ITER_NO_MORE)
pkiDebug("%s: We thought there were %d certs, but "
"crypto_cert_iteration_next stopped after %d?\n",
__FUNCTION__, cert_count, i);
else
pkiDebug("%s: crypto_cert_iteration_next error %d, %s\n",
__FUNCTION__, retval, error_message(retval));
goto cleanup;
}
retval = crypto_cert_get_matching_data(context, ch, &matchdata[i]);
if (retval) {
pkiDebug("%s: crypto_cert_get_matching_data error %d, %s\n",
__FUNCTION__, retval, error_message(retval));
goto cleanup;
}
}
*all_matching_data = matchdata;
retval = 0;
cleanup:
if (ih != NULL)
crypto_cert_iteration_end(context, ih);
if (retval) {
if (matchdata != NULL)
free_all_cert_matching_data(context, matchdata);
}
pkiDebug("%s: returning %d, certinfo %p\n",
__FUNCTION__, retval, *all_matching_data);
return retval;
}
krb5_error_code
pkinit_cert_matching(krb5_context context,
pkinit_plg_crypto_context plg_cryptoctx,
pkinit_req_crypto_context req_cryptoctx,
pkinit_identity_crypto_context id_cryptoctx,
krb5_principal princ)
{
krb5_error_code retval = KRB5KDC_ERR_PREAUTH_FAILED;
int x;
char **rules = NULL;
rule_set *rs = NULL;
int match_found = 0;
pkinit_cert_matching_data **matchdata = NULL;
pkinit_cert_matching_data *the_matching_cert = NULL;
pkinit_libdefault_strings(context, krb5_princ_realm(context, princ),
"pkinit_cert_match", &rules);
if (rules == NULL) {
pkiDebug("%s: no matching rules found in config file\n", __FUNCTION__);
retval = crypto_cert_select_default(context, plg_cryptoctx,
req_cryptoctx, id_cryptoctx);
goto cleanup;
}
for (x = 0; rules[x] != NULL; x++) {
pkiDebug("%s: Processing rule '%s'\n", __FUNCTION__, rules[x]);
if (rs != NULL) {
free_rule_set(context, rs);
rs = NULL;
}
retval = parse_rule_set(context, rules[x], &rs);
if (retval) {
if (retval == EINVAL) {
pkiDebug("%s: Ignoring invalid rule pkinit_cert_match = '%s'\n",
__FUNCTION__, rules[x]);
continue;
}
goto cleanup;
}
if (matchdata == NULL) {
retval = obtain_all_cert_matching_data(context, plg_cryptoctx,
req_cryptoctx, id_cryptoctx,
&matchdata);
if (retval || matchdata == NULL) {
pkiDebug("%s: Error %d obtaining certificate information\n",
__FUNCTION__, retval);
retval = ENOENT;
goto cleanup;
}
}
retval = check_all_certs(context, plg_cryptoctx, req_cryptoctx,
id_cryptoctx, princ, rs, matchdata,
&match_found, &the_matching_cert);
if (retval) {
pkiDebug("%s: Error %d, checking certs against rule '%s'\n",
__FUNCTION__, retval, rules[x]);
goto cleanup;
}
if (match_found) {
pkiDebug("%s: We have an exact match with rule '%s'\n",
__FUNCTION__, rules[x]);
break;
}
}
if (match_found && the_matching_cert != NULL) {
pkiDebug("%s: Selecting the matching cert!\n", __FUNCTION__);
retval = crypto_cert_select(context, the_matching_cert);
if (retval) {
pkiDebug("%s: crypto_cert_select error %d, %s\n",
__FUNCTION__, retval, error_message(retval));
goto cleanup;
}
} else {
retval = ENOENT;
goto cleanup;
}
retval = 0;
cleanup:
if (rules != NULL)
profile_free_list(rules);
if (rs != NULL)
free_rule_set(context, rs);
if (matchdata != NULL)
free_all_cert_matching_data(context, matchdata);
return retval;
}