From 2257b8bbedef41cdafb88aa2cc74969efe247a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Pitucha?= Date: Thu, 16 Jun 2016 16:15:59 +1000 Subject: [PATCH] Allow custom domain labels Original validator checked for domain labels as defined by RFC1034, however real internet deals with other domains as well - starting with digits or symbols. This change allows modifying the pattern to allow custom / relaxed rules. Validation has been removed from adding a domain to a new extension, since it's only used in fixups and the domain should be already validated. (or not, if not configured) Closes-bug: 1592489 Change-Id: Ib453054ba5f554bab28cff392c539e713fa28918 --- anchor/X509/extension.py | 6 ------ anchor/util.py | 10 ++-------- anchor/validators/standards.py | 20 ++++++++++++++++---- config.json | 4 +++- doc/source/validators.rst | 4 ++++ tests/X509/test_extension.py | 5 ----- tests/validators/test_standards_validator.py | 5 +++++ 7 files changed, 30 insertions(+), 24 deletions(-) diff --git a/anchor/X509/extension.py b/anchor/X509/extension.py index 9111c6b..b00ed51 100644 --- a/anchor/X509/extension.py +++ b/anchor/X509/extension.py @@ -24,7 +24,6 @@ from pyasn1.type import tag as asn1_tag from pyasn1.type import univ as asn1_univ from anchor.asn1 import rfc5280 -from anchor import util as a_utils from anchor.X509 import errors from anchor.X509 import utils @@ -331,11 +330,6 @@ class X509ExtensionSubjectAltName(X509Extension): @modifies_ext_value def add_dns_id(self, dns_id, validate=True, ext_value=None): - if validate: - try: - a_utils.verify_domain(dns_id, allow_wildcards=True) - except ValueError as e: - raise errors.X509Error("invalid domain provided: %s" % str(e)) new_pos = len(ext_value) ext_value[new_pos] = None ext_value[new_pos]['dNSName'] = dns_id diff --git a/anchor/util.py b/anchor/util.py index 82756d3..437bf46 100644 --- a/anchor/util.py +++ b/anchor/util.py @@ -15,18 +15,12 @@ from __future__ import absolute_import import base64 import os -import re import stat from anchor import errors -# RFC1034 allows a simple " " too, but it's not allowed in certificates, so it -# will not match -RE_DOMAIN_LABEL = re.compile("^[a-z](?:[-a-z0-9]*[a-z0-9])?$", re.IGNORECASE) - - -def verify_domain(domain, allow_wildcards=False): +def verify_domain(domain, label_re_comp, allow_wildcards=False): labels = domain.split('.') if labels[-1] == "": # single trailing . is ok, ignore @@ -44,7 +38,7 @@ def verify_domain(domain, allow_wildcards=False): "domain <%s> has wildcard that's not in the " "left-most label (RFC6125/6.4.3)" % (domain,)) else: - if RE_DOMAIN_LABEL.match(label) is None: + if label_re_comp.match(label) is None: raise ValueError( "domain <%s> contains invalid characters " "(RFC1034/3.5)" % (domain,)) diff --git a/anchor/validators/standards.py b/anchor/validators/standards.py index bfda380..5fe6c0d 100644 --- a/anchor/validators/standards.py +++ b/anchor/validators/standards.py @@ -28,12 +28,22 @@ from anchor.validators import errors from anchor.X509 import errors as x509_errors from anchor.X509 import extension +import re -def standards_compliance(csr=None, **kwargs): + +# RFC1034 allows a simple " " too, but it's not allowed in certificates, so it +# will not match +# +# This pattern is RFC1034 compatible otherwise, but since newer RFCs actually +# allow any binary value as the domain label, some operators may want to relax +# the pattern in the configuration, for example to allow leading digits or +# hyphens. +def standards_compliance(csr=None, label_re="^[a-z](?:[-a-z0-9]*[a-z0-9])?$", + **kwargs): """Collection of separate cases of standards validation.""" _no_extension_duplicates(csr) _critical_flags(csr) - _valid_domains(csr) + _valid_domains(csr, label_re) _csr_signature(csr) # TODO(stan): validate srv/uri, distinct DNs, email format, identity keys @@ -67,7 +77,7 @@ def _critical_flags(csr): "(RFC5280/4.1.2.9)") -def _valid_domains(csr): +def _valid_domains(csr, label_re="^[a-z](?:[-a-z0-9]*[a-z0-9])?$"): """Format of the domin names See RFC5280 section 4.2.1.6 / RFC6125 / RFC1034 @@ -76,10 +86,12 @@ def _valid_domains(csr): if not sans: return + label_re_comp = re.compile(label_re, re.IGNORECASE) + ext = sans[0] for domain in ext.get_dns_ids(): try: - util.verify_domain(domain, allow_wildcards=True) + util.verify_domain(domain, label_re_comp, allow_wildcards=True) except ValueError as e: raise errors.ValidationError(str(e)) diff --git a/config.json b/config.json index 06eda78..38c5044 100644 --- a/config.json +++ b/config.json @@ -21,7 +21,9 @@ "authentication": "method_1", "signing_ca": "local", "validators": { - "standards_compliance": {}, + "standards_compliance": { + "label_re": "^[a-z](?:[-a-z0-9]*[a-z0-9])?$" + }, "source_cidrs": { "cidrs": ["127.0.0.0/8"] } diff --git a/doc/source/validators.rst b/doc/source/validators.rst index 3602847..1c895b2 100644 --- a/doc/source/validators.rst +++ b/doc/source/validators.rst @@ -21,6 +21,10 @@ The following validators are implemented at the moment: Any requests produced using standard tooling that fail this check should be reported as Anchor issues. + Parameters: + + - ``label_re``: pattern for acceptable domain label format + ``whitelist_names`` Verifies: CSR. Parameters: diff --git a/tests/X509/test_extension.py b/tests/X509/test_extension.py index 7955739..e532846 100644 --- a/tests/X509/test_extension.py +++ b/tests/X509/test_extension.py @@ -146,11 +146,6 @@ class TestSubjectAltName(unittest.TestCase): self.ext.add_ip(self.ip) self.assertEqual([self.domain], self.ext.get_dns_ids()) - def test_add_dns_id_validation(self): - self.ext.add_dns_id("good.exapmle.com") - with self.assertRaises(errors.X509Error): - self.ext.add_dns_id("-blah") - def test_ips(self): self.ext.add_dns_id(self.domain) self.ext.add_ip(self.ip) diff --git a/tests/validators/test_standards_validator.py b/tests/validators/test_standards_validator.py index 060f602..094f7fd 100644 --- a/tests/validators/test_standards_validator.py +++ b/tests/validators/test_standards_validator.py @@ -163,6 +163,11 @@ class TestValidDomains(unittest.TestCase): with self.assertRaises(errors.ValidationError): standards._valid_domains(csr) + def test_custom_re(self): + csr = self._create_csr_with_domain_san('123.example.com.') + with self.assertRaises(errors.ValidationError): + standards._valid_domains(csr, "^\s\+$") + class TestCsrSignature(tests.DefaultRequestMixin, unittest.TestCase): def test_csr_signature(self):