From 770f4ccee12d4777216628d46ed3b14237708ec5 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 9 May 2019 13:09:40 +0200 Subject: [PATCH] identification: Optionally match RDNs in any order and accept missing RDNs --- conf/options/charon.opt | 16 ++ .../tests/suites/test_identification.c | 113 +++++++- src/libstrongswan/utils/identification.c | 250 ++++++++++++++++-- 3 files changed, 338 insertions(+), 41 deletions(-) diff --git a/conf/options/charon.opt b/conf/options/charon.opt index 21f6e08333..9bf09674df 100644 --- a/conf/options/charon.opt +++ b/conf/options/charon.opt @@ -296,6 +296,22 @@ charon.processor.priority_threads {} Section to configure the number of reserved threads per priority class see JOB PRIORITY MANAGEMENT in **strongswan.conf**(5). +charon.rdn_matching = strict + How RDNs in subject DNs of certificates are matched against configured + identities (_strict_, _reordered_, or _relaxed_). + + How RDNs in subject DNs of certificates are matched against configured + identities. Possible values are _strict_ (the default), _reordered_, and + _relaxed_. With _strict_ the number, type and order of all RDNs has to + match, wildcards (*) for the values of RDNs are allowed (that's the case + for all three variants). Using _reordered_ also matches DNs if the RDNs + appear in a different order, the number and type still has to match. + Finally, _relaxed_ also allows matches of DNs that contain more RDNs than + the configured identity (missing RDNs are treated like a wildcard match). + + Note that _reordered_ and _relaxed_ impose a considerable overhead on memory + usage and runtime, in particular, for mismatches, compared to _static_. + charon.receive_delay = 0 Delay in ms for receiving packets, to simulate larger RTT. diff --git a/src/libstrongswan/tests/suites/test_identification.c b/src/libstrongswan/tests/suites/test_identification.c index feadcc9d93..8560f9ea7c 100644 --- a/src/libstrongswan/tests/suites/test_identification.c +++ b/src/libstrongswan/tests/suites/test_identification.c @@ -626,23 +626,111 @@ static bool id_matches(identification_t *a, char *b_str, id_match_t expected) return match == expected; } +static char* rdn_matching[] = { NULL, "reordered", "relaxed" }; + +static struct { + char *id; + id_match_t match[3]; +} matches_data[] = { + /* C=CH, E=moon@strongswan.org, CN=moon */ + { "C=CH, E=moon@strongswan.org, CN=moon", { + ID_MATCH_PERFECT, ID_MATCH_PERFECT, ID_MATCH_PERFECT }}, + { "C=CH, email=moon@strongswan.org, CN=moon", { + ID_MATCH_PERFECT, ID_MATCH_PERFECT, ID_MATCH_PERFECT }}, + { "C=CH, emailAddress=moon@strongswan.org, CN=moon", { + ID_MATCH_PERFECT, ID_MATCH_PERFECT, ID_MATCH_PERFECT }}, + { "CN=moon, C=CH, E=moon@strongswan.org", { + ID_MATCH_NONE, ID_MATCH_PERFECT, ID_MATCH_PERFECT }}, + { "C=CH, E=*@strongswan.org, CN=moon", { + ID_MATCH_NONE, ID_MATCH_NONE, ID_MATCH_NONE }}, + { "C=CH, E=*, CN=moon", { + ID_MATCH_ONE_WILDCARD, ID_MATCH_ONE_WILDCARD, ID_MATCH_ONE_WILDCARD }}, + { "C=CH, E=*, CN=*", { + ID_MATCH_ONE_WILDCARD - 1, ID_MATCH_ONE_WILDCARD - 1, ID_MATCH_ONE_WILDCARD - 1 }}, + { "C=*, E=*, CN=*", { + ID_MATCH_ONE_WILDCARD - 2, ID_MATCH_ONE_WILDCARD - 2, ID_MATCH_ONE_WILDCARD - 2 }}, + { "C=*, E=*, CN=*, O=BADInc", { + ID_MATCH_NONE, ID_MATCH_NONE, ID_MATCH_NONE }}, + { "C=CH, CN=*", { + ID_MATCH_NONE, ID_MATCH_NONE, ID_MATCH_ONE_WILDCARD - 1 }}, + { "C=*, E=*", { + ID_MATCH_NONE, ID_MATCH_NONE, ID_MATCH_ONE_WILDCARD - 2 }}, + { "C=*, E=a@b.c, CN=*", { + ID_MATCH_NONE, ID_MATCH_NONE, ID_MATCH_NONE }}, + { "C=CH, O=strongSwan, E=*, CN=*", { + ID_MATCH_NONE, ID_MATCH_NONE, ID_MATCH_NONE }}, + { "", { + ID_MATCH_ANY, ID_MATCH_ANY, ID_MATCH_ANY }}, + { "%any", { + ID_MATCH_ANY, ID_MATCH_ANY, ID_MATCH_ANY }}, +}; + START_TEST(test_matches) { identification_t *a; + int i; + + if (rdn_matching[_i]) + { + lib->settings->set_str(lib->settings, "%s.rdn_matching", + rdn_matching[_i], lib->ns); + } a = identification_create_from_string("C=CH, E=moon@strongswan.org, CN=moon"); - ck_assert(id_matches(a, "C=CH, E=moon@strongswan.org, CN=moon", ID_MATCH_PERFECT)); - ck_assert(id_matches(a, "C=CH, email=moon@strongswan.org, CN=moon", ID_MATCH_PERFECT)); - ck_assert(id_matches(a, "C=CH, emailAddress=moon@strongswan.org, CN=moon", ID_MATCH_PERFECT)); - ck_assert(id_matches(a, "C=CH, E=*@strongswan.org, CN=moon", ID_MATCH_NONE)); - ck_assert(id_matches(a, "C=CH, E=*, CN=moon", ID_MATCH_ONE_WILDCARD)); - ck_assert(id_matches(a, "C=CH, E=*, CN=*", ID_MATCH_ONE_WILDCARD - 1)); - ck_assert(id_matches(a, "C=*, E=*, CN=*", ID_MATCH_ONE_WILDCARD - 2)); - ck_assert(id_matches(a, "C=*, E=*, CN=*, O=BADInc", ID_MATCH_NONE)); - ck_assert(id_matches(a, "C=*, E=*", ID_MATCH_NONE)); - ck_assert(id_matches(a, "C=*, E=a@b.c, CN=*", ID_MATCH_NONE)); - ck_assert(id_matches(a, "%any", ID_MATCH_ANY)); + for (i = 0; i < countof(matches_data); i++) + { + ck_assert(id_matches(a, matches_data[i].id, matches_data[i].match[_i])); + } + + a->destroy(a); +} +END_TEST + +static struct { + char *id; + id_match_t match[3]; +} matches_two_ou_data[] = { + /* C=CH, OU=Research, OU=Floor A, CN=moon */ + { "C=CH, OU=Research, OU=Floor A, CN=moon", { + ID_MATCH_PERFECT, ID_MATCH_PERFECT, ID_MATCH_PERFECT }}, + { "C=CH, OU=Floor A, CN=moon", { + ID_MATCH_NONE, ID_MATCH_NONE, ID_MATCH_ONE_WILDCARD }}, + { "C=CH, CN=moon", { + ID_MATCH_NONE, ID_MATCH_NONE, ID_MATCH_ONE_WILDCARD - 1 }}, + { "C=CH, OU=*, CN=moon", { + ID_MATCH_NONE, ID_MATCH_NONE, ID_MATCH_ONE_WILDCARD - 1 }}, + { "C=CH, OU=*, OU=*, CN=moon", { + ID_MATCH_ONE_WILDCARD - 1, ID_MATCH_ONE_WILDCARD - 1, ID_MATCH_ONE_WILDCARD - 1 }}, + { "C=CH, OU=Research, OU=*, CN=moon", { + ID_MATCH_ONE_WILDCARD, ID_MATCH_ONE_WILDCARD, ID_MATCH_ONE_WILDCARD }}, + { "C=CH, OU=*, OU=Floor A, CN=moon", { + ID_MATCH_ONE_WILDCARD, ID_MATCH_ONE_WILDCARD, ID_MATCH_ONE_WILDCARD }}, + { "C=CH, OU=*, OU=Research, CN=moon", { + ID_MATCH_NONE, ID_MATCH_ONE_WILDCARD, ID_MATCH_ONE_WILDCARD }}, + { "C=CH, OU=Floor A, OU=*, CN=moon", { + ID_MATCH_NONE, ID_MATCH_ONE_WILDCARD, ID_MATCH_ONE_WILDCARD }}, + { "C=CH, OU=Floor A, OU=Research, CN=moon", { + ID_MATCH_NONE, ID_MATCH_PERFECT, ID_MATCH_PERFECT }}, +}; + +START_TEST(test_matches_two_ou) +{ + identification_t *a; + int i; + + if (rdn_matching[_i]) + { + lib->settings->set_str(lib->settings, "%s.rdn_matching", + rdn_matching[_i], lib->ns); + } + + a = identification_create_from_string("C=CH, OU=Research, OU=Floor A, CN=moon"); + + for (i = 0; i < countof(matches_two_ou_data); i++) + { + ck_assert(id_matches(a, matches_two_ou_data[i].id, matches_two_ou_data[i].match[_i])); + } a->destroy(a); } @@ -1094,7 +1182,8 @@ Suite *identification_suite_create() suite_add_tcase(s, tc); tc = tcase_create("matches"); - tcase_add_test(tc, test_matches); + tcase_add_loop_test(tc, test_matches, 0, countof(rdn_matching)); + tcase_add_loop_test(tc, test_matches_two_ou, 0, countof(rdn_matching)); tcase_add_test(tc, test_matches_any); tcase_add_test(tc, test_matches_binary); tcase_add_test(tc, test_matches_range); diff --git a/src/libstrongswan/utils/identification.c b/src/libstrongswan/utils/identification.c index 36c0c9daad..3d803c04bb 100644 --- a/src/libstrongswan/utils/identification.c +++ b/src/libstrongswan/utils/identification.c @@ -1,6 +1,6 @@ /* * Copyright (C) 2016 Andreas Steffen - * Copyright (C) 2009-2015 Tobias Brunner + * Copyright (C) 2009-2019 Tobias Brunner * Copyright (C) 2005-2009 Martin Willi * Copyright (C) 2005 Jan Hutter * HSR Hochschule fuer Technik Rapperswil @@ -26,6 +26,7 @@ #include #include #include +#include ENUM_BEGIN(id_match_names, ID_MATCH_NONE, ID_MATCH_MAX_WILDCARDS, "MATCH_NONE", @@ -567,6 +568,14 @@ METHOD(identification_t, get_type, id_type_t, return this->type; } +/** + * Check if this is a wildcard value + */ +static inline bool is_wildcard(chunk_t data) +{ + return data.len == 1 && data.ptr[0] == '*'; +} + METHOD(identification_t, contains_wildcards_dn, bool, private_identification_t *this) { @@ -578,7 +587,7 @@ METHOD(identification_t, contains_wildcards_dn, bool, enumerator = create_part_enumerator(this); while (enumerator->enumerate(enumerator, &type, &data)) { - if (data.len == 1 && data.ptr[0] == '*') + if (is_wildcard(data)) { contains = TRUE; break; @@ -622,7 +631,172 @@ METHOD(identification_t, equals_binary, bool, } /** - * Compare to DNs, for equality if wc == NULL, for match otherwise + * Compare two RDNs for equality, comparing some string types case insensitive + */ +static bool rdn_equals(chunk_t oid, u_char a_type, chunk_t a, u_char b_type, + chunk_t b) +{ + if (a_type == b_type && + (a_type == ASN1_PRINTABLESTRING || + (a_type == ASN1_IA5STRING && + asn1_known_oid(oid) == OID_EMAIL_ADDRESS))) + { /* ignore case for printableStrings and email RDNs */ + return strncaseeq(a.ptr, b.ptr, a.len); + } + else + { /* respect case and length for everything else */ + return memeq(a.ptr, b.ptr, a.len); + } +} + +/** + * RDNs when matching DNs + */ +typedef struct { + chunk_t oid; + u_char type; + chunk_t data; + bool matched; +} rdn_t; + +/** + * Match DNs (o_dn may contain wildcards and RDNs in a different order, if + * allow_unmatched is TRUE, t_dn may contain unmatched RDNs) + */ +static bool match_dn(chunk_t t_dn, chunk_t o_dn, int *wc, bool allow_unmatched) +{ + enumerator_t *enumerator; + array_t *rdns; + rdn_t *rdn, *found; + chunk_t oid, data; + u_char type; + bool finished = FALSE; + int i, regular = 0; + + *wc = 0; + + /* try a binary compare */ + if (chunk_equals(t_dn, o_dn)) + { + return TRUE; + } + + rdns = array_create(0, 8); + + enumerator = create_rdn_enumerator(o_dn); + while (TRUE) + { + if (!enumerator->enumerate(enumerator, &oid, &type, &data)) + { + break; + } + INIT(rdn, + .oid = oid, + .type = type, + .data = data, + ); + if (is_wildcard(data)) + { + /* insert wildcards at the end, to perform exact matches first */ + array_insert(rdns, ARRAY_TAIL, rdn); + } + else + { + array_insert(rdns, regular++, rdn); + } + /* the enumerator returns FALSE on parse error, we are finished + * if we have reached the end of the DN only */ + if ((data.ptr + data.len == o_dn.ptr + o_dn.len)) + { + finished = TRUE; + } + } + enumerator->destroy(enumerator); + + if (!finished) + { /* invalid DN */ + array_destroy_function(rdns, (void*)free, NULL); + return FALSE; + } + finished = FALSE; + + enumerator = create_rdn_enumerator(t_dn); + while (TRUE) + { + if (!enumerator->enumerate(enumerator, &oid, &type, &data)) + { + break; + } + for (i = 0, found = NULL; i < array_count(rdns); i++) + { + array_get(rdns, i, &rdn); + if (!rdn->matched && chunk_equals(rdn->oid, oid)) + { + if (is_wildcard(rdn->data)) + { + (*wc)++; + } + else if (data.len != rdn->data.len || + !rdn_equals(oid, type, data, rdn->type, rdn->data)) + { + continue; + } + rdn->matched = TRUE; + found = rdn; + break; + } + } + if (!found) + { + /* treat unmatched RDNs like wildcards if allowed */ + if (!allow_unmatched) + { + break; + } + (*wc)++; + } + /* the enumerator returns FALSE on parse error, we are finished + * if we have reached the end of the DN only */ + if ((data.ptr + data.len == t_dn.ptr + t_dn.len)) + { + finished = TRUE; + } + } + enumerator->destroy(enumerator); + + if (finished) + { + for (i = 0; i < array_count(rdns); i++) + { + array_get(rdns, i, &rdn); + if (!rdn->matched) + { + finished = FALSE; + } + } + } + array_destroy_function(rdns, (void*)free, NULL); + return finished; +} + +/** + * Reordered RDNs are fine, but match all + */ +static bool match_dn_reordered(chunk_t t_dn, chunk_t o_dn, int *wc) +{ + return match_dn(t_dn, o_dn, wc, FALSE); +} + +/** + * t_dn may contain more RDNs than o_dn + */ +static bool match_dn_relaxed(chunk_t t_dn, chunk_t o_dn, int *wc) +{ + return match_dn(t_dn, o_dn, wc, TRUE); +} + +/** + * Compare two DNs, for equality if wc == NULL, with wildcard matching otherwise */ static bool compare_dn(chunk_t t_dn, chunk_t o_dn, int *wc) { @@ -635,14 +809,11 @@ static bool compare_dn(chunk_t t_dn, chunk_t o_dn, int *wc) { *wc = 0; } - else + else if (t_dn.len != o_dn.len) { - if (t_dn.len != o_dn.len) - { - return FALSE; - } + return FALSE; } - /* try a binary compare */ + if (chunk_equals(t_dn, o_dn)) { return TRUE; @@ -668,7 +839,7 @@ static bool compare_dn(chunk_t t_dn, chunk_t o_dn, int *wc) { break; } - if (wc && o_data.len == 1 && o_data.ptr[0] == '*') + if (wc && is_wildcard(o_data)) { (*wc)++; } @@ -678,22 +849,9 @@ static bool compare_dn(chunk_t t_dn, chunk_t o_dn, int *wc) { break; } - if (t_type == o_type && - (t_type == ASN1_PRINTABLESTRING || - (t_type == ASN1_IA5STRING && - asn1_known_oid(t_oid) == OID_EMAIL_ADDRESS))) - { /* ignore case for printableStrings and email RDNs */ - if (strncasecmp(t_data.ptr, o_data.ptr, t_data.len) != 0) - { - break; - } - } - else - { /* respect case and length for everything else */ - if (!memeq(t_data.ptr, o_data.ptr, t_data.len)) - { - break; - } + if (!rdn_equals(t_oid, t_type, t_data, o_type, o_data)) + { + break; } } /* the enumerator returns FALSE on parse error, we are finished @@ -817,8 +975,12 @@ METHOD(identification_t, matches_any, id_match_t, return ID_MATCH_NONE; } -METHOD(identification_t, matches_dn, id_match_t, - private_identification_t *this, identification_t *other) +/** + * Match DNs given the matching function + */ +static id_match_t matches_dn_internal(private_identification_t *this, + identification_t *other, + bool (*match)(chunk_t,chunk_t,int*)) { int wc; @@ -829,7 +991,7 @@ METHOD(identification_t, matches_dn, id_match_t, if (this->type == other->get_type(other)) { - if (compare_dn(this->encoded, other->get_encoding(other), &wc)) + if (match(this->encoded, other->get_encoding(other), &wc)) { wc = min(wc, ID_MATCH_ONE_WILDCARD - ID_MATCH_MAX_WILDCARDS); return ID_MATCH_PERFECT - wc; @@ -838,6 +1000,24 @@ METHOD(identification_t, matches_dn, id_match_t, return ID_MATCH_NONE; } +METHOD(identification_t, matches_dn, id_match_t, + private_identification_t *this, identification_t *other) +{ + return matches_dn_internal(this, other, compare_dn); +} + +METHOD(identification_t, matches_dn_reordered, id_match_t, + private_identification_t *this, identification_t *other) +{ + return matches_dn_internal(this, other, match_dn_reordered); +} + +METHOD(identification_t, matches_dn_relaxed, id_match_t, + private_identification_t *this, identification_t *other) +{ + return matches_dn_internal(this, other, match_dn_relaxed); +} + /** * Transform netmask to CIDR bits */ @@ -1150,6 +1330,7 @@ METHOD(identification_t, destroy, void, static private_identification_t *identification_create(id_type_t type) { private_identification_t *this; + char *rdn_matching; INIT(this, .public = { @@ -1182,6 +1363,17 @@ static private_identification_t *identification_create(id_type_t type) this->public.equals = _equals_dn; this->public.matches = _matches_dn; this->public.contains_wildcards = _contains_wildcards_dn; + /* check for more relaxed matching config */ + rdn_matching = lib->settings->get_str(lib->settings, + "%s.rdn_matching", NULL, lib->ns); + if (streq("reordered", rdn_matching)) + { + this->public.matches = _matches_dn_reordered; + } + else if (streq("relaxed", rdn_matching)) + { + this->public.matches = _matches_dn_relaxed; + } break; case ID_IPV4_ADDR: case ID_IPV6_ADDR: -- 2.47.2