diff --git a/certbot/src/certbot/_internal/san.py b/certbot/src/certbot/_internal/san.py index ecd0cd212..96ba4e2fa 100644 --- a/certbot/src/certbot/_internal/san.py +++ b/certbot/src/certbot/_internal/san.py @@ -6,7 +6,7 @@ from typing import Any, Iterable from acme import crypto_util as acme_crypto_util from cryptography import x509 -from certbot import errors +from certbot.util import enforce_domain_sanity class SAN: """A domain or IP address. @@ -27,49 +27,7 @@ class DNSName(SAN): def __init__(self, dns_name: str) -> None: if not isinstance(dns_name, str): raise TypeError("tried to initialize DNSName with non-str") - try: - dns_name.encode('ascii') - except UnicodeError: - raise errors.ConfigurationError("Non-ASCII domain names not supported. " - "To issue for an Internationalized Domain Name, use Punycode.") - dns_name = dns_name.lower() - # Remove trailing dot - dns_name = dns_name.removesuffix(".") - - # Separately check for odd "domains" like "http://example.com" to fail - # fast and provide a clear error message - for scheme in ["http", "https"]: # Other schemes seem unlikely - if dns_name.startswith("{0}://".format(scheme)): - raise errors.ConfigurationError( - "Requested name {0} appears to be a URL, not a FQDN. " - "Try again without the leading \"{1}://\".".format( - dns_name, scheme - ) - ) - - try: - IPAddress(dns_name) - raise errors.ConfigurationError( - "Requested name {0} is an IP address. The Let's Encrypt " - "certificate authority will not issue certificates for a " - "bare IP address.".format(dns_name)) - except ValueError: - pass - - # FQDN checks according to RFC 2181: domain name should be less than 255 - # octets (inclusive). And each label is 1 - 63 octets (inclusive). - # https://tools.ietf.org/html/rfc2181#section-11 - msg = "Requested domain {0} is not a FQDN because".format(dns_name) - if len(dns_name) > 255: - raise errors.ConfigurationError("{0} it is too long.".format(msg)) - labels = dns_name.split('.') - for l in labels: - if not l: - raise errors.ConfigurationError("{0} it contains an empty label.".format(msg)) - if len(l) > 63: - raise errors.ConfigurationError("{0} label {1} is too long.".format(msg, l)) - - self.dns_name = dns_name + self.dns_name = enforce_domain_sanity(dns_name) def __str__(self) -> str: return self.dns_name diff --git a/certbot/src/certbot/_internal/san_test.py b/certbot/src/certbot/_internal/san_test.py index c14714f4a..83190d554 100644 --- a/certbot/src/certbot/_internal/san_test.py +++ b/certbot/src/certbot/_internal/san_test.py @@ -10,7 +10,6 @@ from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec -from certbot import errors from certbot._internal import san class SanTest(unittest.TestCase): @@ -66,58 +65,6 @@ class SanTest(unittest.TestCase): with pytest.raises(ValueError): san.IPAddress("example.com") -class EnforceDomainSyntaxTest(unittest.TestCase): - """Test validation of domain names.""" - def _call(self, dns_name: str) -> None: - san.DNSName(dns_name) - - def test_nonascii_str(self) -> None: - with pytest.raises(errors.ConfigurationError): - self._call("eichh\u00f6rnchen.example.com") - - def test_too_long(self) -> None: - long_domain = "a"*256 - with pytest.raises(errors.ConfigurationError): - self._call(long_domain) - - def test_not_too_long(self) -> None: - not_too_long_domain = "{0}.{1}.{2}.{3}".format("a"*63, "b"*63, "c"*63, "d"*63) - self._call(not_too_long_domain) - - def test_empty_label(self) -> None: - empty_label_domain = "fizz..example.com" - with pytest.raises(errors.ConfigurationError): - self._call(empty_label_domain) - - def test_empty_trailing_label(self) -> None: - empty_trailing_label_domain = "example.com.." - with pytest.raises(errors.ConfigurationError): - self._call(empty_trailing_label_domain) - - def test_long_label_1(self) -> None: - long_label_domain = "a"*64 - with pytest.raises(errors.ConfigurationError): - self._call(long_label_domain) - - def test_long_label_2(self) -> None: - long_label_domain = "{0}.{1}.com".format("a"*64, "b"*63) - with pytest.raises(errors.ConfigurationError): - self._call(long_label_domain) - - def test_not_long_label(self) -> None: - not_too_long_label_domain = "{0}.{1}.com".format("a"*63, "b"*63) - self._call(not_too_long_label_domain) - - def test_empty_domain(self) -> None: - empty_domain = "" - with pytest.raises(errors.ConfigurationError): - self._call(empty_domain) - - def test_punycode_ok(self) -> None: - # Punycode is now legal, so no longer an error; instead check - # that it's _not_ an error (at the initial sanity check stage) - self._call('this.is.xn--ls8h.tld') - class FromX509Test(unittest.TestCase): def test_csr(self) -> None: key = ec.generate_private_key(ec.SECP256R1()) diff --git a/certbot/src/certbot/util.py b/certbot/src/certbot/util.py index b258f0665..801c19647 100644 --- a/certbot/src/certbot/util.py +++ b/certbot/src/certbot/util.py @@ -21,7 +21,6 @@ import configargparse from certbot import errors from certbot._internal import constants from certbot._internal import lock -from certbot._internal import san from certbot.compat import filesystem from certbot.compat import os @@ -577,8 +576,7 @@ def enforce_le_validity(domain: str) -> str: """ - # Do basic validation on a DNSName - domain = san.DNSName(domain).dns_name + domain = enforce_domain_sanity(domain) if not re.match("^[A-Za-z0-9.-]*$", domain): raise errors.ConfigurationError( "{0} contains an invalid character. " diff --git a/newsfragments/10519.changed b/newsfragments/10519.changed new file mode 100644 index 000000000..7f1f75a54 --- /dev/null +++ b/newsfragments/10519.changed @@ -0,0 +1 @@ +san.DNSName now calls util.enforce_domain_sanity to reduce code duplication