Merge pull request #10519 from jsha/dedupe-enforce-domain-sanity

De-duplicate enforce_domain_sanity
This commit is contained in:
Jacob Hoffman-Andrews 2025-12-16 10:31:25 -08:00 committed by GitHub
parent fb3e95e372
commit 17a1f0e114
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 4 additions and 100 deletions

View file

@ -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

View file

@ -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())

View file

@ -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. "

View file

@ -0,0 +1 @@
san.DNSName now calls util.enforce_domain_sanity to reduce code duplication