/*
SSSD
Library for rule based certificate to user mapping - KRB5 matching rules
Authors:
Sumit Bose <sbose@redhat.com>
Copyright (C) 2017 Red Hat
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <ctype.h>
#include "util/util.h"
#include "util/crypto/sss_crypto.h"
#include "lib/certmap/sss_certmap.h"
#include "lib/certmap/sss_certmap_int.h"
static bool is_dotted_decimal(const char *s, size_t len)
{
size_t c = 0;
bool has_dot = false;
if (s == NULL || !isdigit(s[c++])) {
return false;
}
while ((len == 0 && s[c] != '\0') || (len != 0 && c < len)) {
if (s[c] != '.' && !isdigit(s[c])) {
return false;
}
if (!has_dot && s[c] == '.') {
has_dot = true;
}
c++;
}
return (has_dot && isdigit(s[c - 1]));
}
static int component_list_destructor(void *data)
{
struct component_list *comp = talloc_get_type(data, struct component_list);
if (comp != NULL) {
regfree(&(comp->regexp));
}
return 0;
}
/*
* The syntax of the MIT Kerberos style matching rules is:
* [KRB5:][relation-operator]component-rule ...
*
* where:
*
* relation-operator
* can be either &&, meaning all component rules must match, or ||,
* meaning only one component rule must match. The default is &&.
*
* component-rule
* can be one of the following. Note that there is no punctuation or whitespace between component rules.
* <SUBJECT>regular-expression
* <ISSUER>regular-expression
* <SAN>regular-expression
* <EKU>extended-key-usage
* <KU>key-usage
*
* see man sss-certmap for more details
*
*/
static int get_comp_value(TALLOC_CTX *mem_ctx,
struct sss_certmap_ctx *ctx,
const char **cur,
struct component_list **_comp)
{
struct component_list *comp = NULL;
const char *end;
int ret;
comp = talloc_zero(mem_ctx, struct component_list);
if (comp == NULL) {
ret = ENOMEM;
goto done;
}
talloc_set_destructor((TALLOC_CTX *) comp, component_list_destructor);
end = strchr(*cur, '<');
if (end == NULL) {
comp->val = talloc_strdup(comp, *cur);
} else {
comp->val = talloc_strndup(comp, *cur, end - *cur);
}
if (comp->val == NULL) {
ret = ENOMEM;
goto done;
}
if (*(comp->val) == '\0') {
CM_DEBUG(ctx, "Missing component value.");
ret = EINVAL;
goto done;
}
*cur += strlen(comp->val);
*_comp = comp;
ret = 0;
done:
if (ret != 0) {
talloc_free(comp);
}
return ret;
}
static int parse_krb5_get_eku_value(TALLOC_CTX *mem_ctx,
struct sss_certmap_ctx *ctx,
const char **cur,
struct component_list **_comp)
{
struct component_list *comp = NULL;
int ret;
char **eku_list;
size_t c;
size_t k;
const char *o;
size_t e = 0;
int eku_list_size;
struct ext_key_usage {
const char *name;
const char *oid;
} ext_key_usage[] = {
/* RFC 3280 section 4.2.1.13 */
{"serverAuth", "1.3.6.1.5.5.7.3.1"},
{"clientAuth", "1.3.6.1.5.5.7.3.2"},
{"codeSigning", "1.3.6.1.5.5.7.3.3"},
{"emailProtection", "1.3.6.1.5.5.7.3.4"},
{"timeStamping", "1.3.6.1.5.5.7.3.8"},
{"OCSPSigning", "1.3.6.1.5.5.7.3.9"},
/* RFC 4556 section 3.2.2 */
{"KPClientAuth", "1.3.6.1.5.2.3.4"},
{"pkinit", "1.3.6.1.5.2.3.4"},
/* https://support.microsoft.com/en-us/help/287547/object-ids-associated-with-microsoft-cryptography*/
{"msScLogin", "1.3.6.1.4.1.311.20.2.2"},
{NULL ,0}
};
ret = get_comp_value(mem_ctx, ctx, cur, &comp);
if (ret != 0) {
CM_DEBUG(ctx, "Failed to parse regexp.");
goto done;
}
ret = split_on_separator(mem_ctx, comp->val, ',', true, true,
&eku_list, &eku_list_size);
if (ret != 0) {
CM_DEBUG(ctx, "Failed to split list.");
goto done;
}
comp->eku_oid_list = talloc_zero_array(comp, const char *,
eku_list_size + 1);
if (comp->eku_oid_list == NULL) {
ret = ENOMEM;
goto done;
}
for (c = 0; eku_list[c] != NULL; c++) {
for (k = 0; ext_key_usage[k].name != NULL; k++) {
CM_DEBUG(ctx, "[%s][%s].", eku_list[c], ext_key_usage[k].name);
if (strcasecmp(eku_list[c], ext_key_usage[k].name) == 0) {
comp->eku_oid_list[e] = talloc_strdup(comp->eku_oid_list,
ext_key_usage[k].oid);
if (comp->eku_oid_list[e] == NULL) {
ret = ENOMEM;
goto done;
}
e++;
break;
}
}
if (ext_key_usage[k].name == NULL) {
/* check for an dotted-decimal OID */
if (*(eku_list[c]) != '.') {
o = eku_list[c];
if (is_dotted_decimal(o, 0)) {
/* looks like a OID, only '.' and digits */
comp->eku_oid_list[e] = talloc_strdup(comp->eku_oid_list,
eku_list[c]);
if (comp->eku_oid_list[e] == NULL) {
ret = ENOMEM;
goto done;
}
e++;
continue;
}
}
CM_DEBUG(ctx, "No matching extended key usage found.");
ret = EINVAL;
goto done;
}
}
if (e == 0) {
talloc_free(comp->eku_oid_list);
comp->eku_oid_list = NULL;
}
ret = 0;
done:
if (ret == 0) {
*_comp = comp;
} else {
talloc_free(comp);
}
return ret;
}
static int parse_krb5_get_ku_value(TALLOC_CTX *mem_ctx,
struct sss_certmap_ctx *ctx,
const char **cur,
struct component_list **_comp)
{
struct component_list *comp = NULL;
int ret;
char **ku_list;
size_t c;
size_t k;
struct key_usage {
const char *name;
uint32_t flag;
} key_usage[] = {
{"digitalSignature" , SSS_KU_DIGITAL_SIGNATURE},
{"nonRepudiation" , SSS_KU_NON_REPUDIATION},
{"keyEncipherment" , SSS_KU_KEY_ENCIPHERMENT},
{"dataEncipherment" , SSS_KU_DATA_ENCIPHERMENT},
{"keyAgreement" , SSS_KU_KEY_AGREEMENT},
{"keyCertSign" , SSS_KU_KEY_CERT_SIGN},
{"cRLSign" , SSS_KU_CRL_SIGN},
{"encipherOnly" , SSS_KU_ENCIPHER_ONLY},
{"decipherOnly" , SSS_KU_DECIPHER_ONLY},
{NULL ,0}
};
ret = get_comp_value(mem_ctx, ctx, cur, &comp);
if (ret != 0) {
CM_DEBUG(ctx, "Failed to get value.");
goto done;
}
ret = split_on_separator(mem_ctx, comp->val, ',', true, true,
&ku_list, NULL);
if (ret != 0) {
CM_DEBUG(ctx, "Failed to split list.");
goto done;
}
for (c = 0; ku_list[c] != NULL; c++) {
for (k = 0; key_usage[k].name != NULL; k++) {
if (strcasecmp(ku_list[c], key_usage[k].name) == 0) {
comp->ku |= key_usage[k].flag;
break;
}
}
if (key_usage[k].name == NULL) {
/* FIXME: add check for numerical ku */
CM_DEBUG(ctx, "No matching key usage found.");
ret = EINVAL;
goto done;
}
}
ret = 0;
done:
if (ret == 0) {
*_comp = comp;
} else {
talloc_free(comp);
}
return ret;
}
static int parse_krb5_get_component_value(TALLOC_CTX *mem_ctx,
struct sss_certmap_ctx *ctx,
const char **cur,
struct component_list **_comp)
{
struct component_list *comp = NULL;
int ret;
ret = get_comp_value(mem_ctx, ctx, cur, &comp);
if (ret != 0) {
CM_DEBUG(ctx, "Failed to parse regexp.");
goto done;
}
ret = regcomp(&(comp->regexp), comp->val, REG_EXTENDED);
if (ret != 0) {
CM_DEBUG(ctx, "Failed to parse regexp.");
goto done;
}
ret = 0;
done:
if (ret == 0) {
*_comp = comp;
} else {
talloc_free(comp);
}
return ret;
}
struct san_name {
const char *name;
enum san_opt san_opt;
bool is_string;
} san_names[] = {
/* https://www.ietf.org/rfc/rfc3280.txt section 4.2.1.7 */
{"otherName", SAN_OTHER_NAME, false},
{"rfc822Name", SAN_RFC822_NAME,true},
{"dNSName", SAN_DNS_NAME, true},
{"x400Address", SAN_X400_ADDRESS, false},
{"directoryName", SAN_DIRECTORY_NAME, true},
{"ediPartyName", SAN_EDIPART_NAME, false},
{"uniformResourceIdentifier", SAN_URI, true},
{"iPAddress", SAN_IP_ADDRESS, true},
{"registeredID", SAN_REGISTERED_ID, true},
/* https://www.ietf.org/rfc/rfc4556.txt section 3.2.2 */
{"pkinitSAN", SAN_PKINIT, true},
/* https://support.microsoft.com/en-us/help/287547/object-ids-associated-with-microsoft-cryptography */
{"ntPrincipalName", SAN_NT, true},
/* both previous principal types */
{"Principal", SAN_PRINCIPAL, true},
{"stringOtherName", SAN_STRING_OTHER_NAME, true},
{NULL, SAN_END, false}
};
static int parse_krb5_get_san_option(TALLOC_CTX *mem_ctx,
struct sss_certmap_ctx *ctx,
const char **cur,
enum san_opt *option,
char **str_other_name_oid)
{
char *end;
size_t c;
size_t len;
end = strchr(*cur, '>');
if (end == NULL) {
CM_DEBUG(ctx, "Failed to parse SAN option.");
return EINVAL;
}
len = end - *cur;
if (len == 0) {
c= SAN_PRINCIPAL;
} else {
for (c = 0; san_names[c].name != NULL; c++) {
if (strncasecmp(*cur, san_names[c].name, len) == 0) {
break;
}
}
if (san_names[c].name == NULL) {
if (is_dotted_decimal(*cur, len)) {
c = SAN_STRING_OTHER_NAME;
*str_other_name_oid = talloc_strndup(mem_ctx, *cur, len);
if (*str_other_name_oid == NULL) {
CM_DEBUG(ctx, "talloc_strndup failed.");
return ENOMEM;
}
} else {
CM_DEBUG(ctx, "Unknown SAN option.");
return EINVAL;
}
}
}
*option = san_names[c].san_opt;
*cur = end + 1;
return 0;
}
static int parse_krb5_get_san_value(TALLOC_CTX *mem_ctx,
struct sss_certmap_ctx *ctx,
const char **cur,
struct component_list **_comp)
{
struct component_list *comp = NULL;
enum san_opt san_opt = SAN_PRINCIPAL;
int ret;
char *str_other_name_oid = NULL;
if (*(*cur - 1) == ':') {
ret = parse_krb5_get_san_option(mem_ctx, ctx, cur, &san_opt,
&str_other_name_oid);
if (ret != 0) {
goto done;
}
}
if (san_names[san_opt].is_string) {
ret = parse_krb5_get_component_value(mem_ctx, ctx, cur, &comp);
if (ret != 0) {
goto done;
}
} else {
ret = get_comp_value(mem_ctx, ctx, cur, &comp);
if (ret != 0) {
goto done;
}
if (comp->val != NULL) {
comp->bin_val = sss_base64_decode(comp, comp->val,
&comp->bin_val_len);
/* for some reasons the NSS version of sss_base64_decode might
* return a non-NULL value on error but len is still 0, so better
* check both. */
if (comp->bin_val == NULL || comp->bin_val_len == 0) {
CM_DEBUG(ctx, "Base64 decode failed.");
ret = EINVAL;
goto done;
}
}
}
comp->san_opt = san_opt;
done:
if (ret == 0) {
comp->str_other_name_oid = talloc_steal(comp, str_other_name_oid);
*_comp = comp;
} else {
talloc_free(comp);
talloc_free(str_other_name_oid);
}
return ret;
}
int parse_krb5_match_rule(struct sss_certmap_ctx *ctx,
const char *rule_start,
struct krb5_match_rule **match_rule)
{
const char *cur;
struct krb5_match_rule *rule;
struct component_list *comp;
int ret;
rule = talloc_zero(ctx, struct krb5_match_rule);
if (rule == NULL) {
ret = ENOMEM;
goto done;
}
cur = rule_start;
/* check relation */
if (strncmp(cur, "&&", 2) == 0) {
rule->r = relation_and;
cur += 2;
} else if (strncmp(cur, "||", 2) == 0) {
rule->r = relation_or;
cur += 2;
} else {
rule->r = relation_and;
}
while (*cur != '\0') {
/* new component must start with '<' */
if (*cur != '<') {
CM_DEBUG(ctx, "Invalid KRB5 matching rule.");
ret = EINVAL;
goto done;
}
cur++;
if (strncmp(cur, "ISSUER>", 7) == 0) {
cur += 7;
ret = parse_krb5_get_component_value(rule, ctx, &cur, &comp);
if (ret != 0) {
goto done;
}
DLIST_ADD(rule->issuer, comp);
} else if (strncmp(cur, "SUBJECT>", 8) == 0) {
cur += 8;
ret = parse_krb5_get_component_value(rule, ctx, &cur, &comp);
if (ret != 0) {
goto done;
}
DLIST_ADD(rule->subject, comp);
} else if (strncmp(cur, "KU>", 3) == 0) {
cur += 3;
ret = parse_krb5_get_ku_value(rule, ctx, &cur, &comp);
if (ret != 0) {
goto done;
}
DLIST_ADD(rule->ku, comp);
} else if (strncmp(cur, "EKU>", 4) == 0) {
cur += 4;
ret = parse_krb5_get_eku_value(rule, ctx, &cur, &comp);
if (ret != 0) {
goto done;
}
DLIST_ADD(rule->eku, comp);
} else if (strncmp(cur, "SAN>", 4) == 0
|| strncmp(cur, "SAN:", 4) == 0) {
cur += 4;
ret = parse_krb5_get_san_value(rule, ctx, &cur, &comp);
if (ret != 0) {
goto done;
}
DLIST_ADD(rule->san, comp);
} else {
CM_DEBUG(ctx, "Invalid KRB5 matching rule.");
ret = EINVAL;
goto done;
}
}
ret = 0;
done:
if (ret == 0) {
*match_rule = rule;
} else {
talloc_free(rule);
}
return ret;
}