identification: Optionally match RDNs in any order and accept missing RDNs
authorTobias Brunner <tobias@strongswan.org>
Thu, 9 May 2019 11:09:40 +0000 (13:09 +0200)
committerTobias Brunner <tobias@strongswan.org>
Mon, 26 Aug 2019 09:15:53 +0000 (11:15 +0200)
conf/options/charon.opt
src/libstrongswan/tests/suites/test_identification.c
src/libstrongswan/utils/identification.c

index 21f6e08..9bf0967 100644 (file)
@@ -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.
 
index feadcc9..8560f9e 100644 (file)
@@ -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);
index 36c0c9d..3d803c0 100644 (file)
@@ -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 <asn1/oid.h>
 #include <asn1/asn1.h>
 #include <crypto/hashers/hasher.h>
+#include <collections/array.h>
 
 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: