1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2024-11-22 02:17:26 +00:00

don't try to get certs for IDNA domains and report all reasons for not fetching a certificate

fixes #646
This commit is contained in:
Joshua Tauberer 2016-01-01 12:10:32 -05:00
parent b6933a73fa
commit b1b57f9bfd

View File

@ -5,6 +5,8 @@ import os, os.path, re, shutil
from utils import shell, safe_domain_name, sort_domains from utils import shell, safe_domain_name, sort_domains
import idna
# SELECTING SSL CERTIFICATES FOR USE IN WEB # SELECTING SSL CERTIFICATES FOR USE IN WEB
def get_ssl_certificates(env): def get_ssl_certificates(env):
@ -155,7 +157,7 @@ def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False
# PROVISIONING CERTIFICATES FROM LETSENCRYPT # PROVISIONING CERTIFICATES FROM LETSENCRYPT
def get_certificates_to_provision(env): def get_certificates_to_provision(env):
# Get a list of domain names that we should now provision certificates # Get a set of domain names that we should now provision certificates
# for. Provision if a domain name has no valid certificate or if any # for. Provision if a domain name has no valid certificate or if any
# certificate is expiring in 14 days. If provisioning anything, also # certificate is expiring in 14 days. If provisioning anything, also
# provision certificates expiring within 30 days. The period between # provision certificates expiring within 30 days. The period between
@ -171,11 +173,13 @@ def get_certificates_to_provision(env):
certs = get_ssl_certificates(env) certs = get_ssl_certificates(env)
domains = set() domains = set()
domains_if_any = set() domains_if_any = set()
problems = { }
for domain in get_web_domains(env): for domain in get_web_domains(env):
try: try:
cert = get_domain_ssl_files(domain, certs, env, allow_missing_cert=True) cert = get_domain_ssl_files(domain, certs, env, allow_missing_cert=True)
except FileNotFoundError: except FileNotFoundError as e:
# system certificate is not present # system certificate is not present
problems[domain] = "Error: " + str(e)
continue continue
if cert is None: if cert is None:
# No valid certificate available. # No valid certificate available.
@ -192,37 +196,49 @@ def get_certificates_to_provision(env):
elif cert.not_valid_after-now < datetime.timedelta(days=30): elif cert.not_valid_after-now < datetime.timedelta(days=30):
domains_if_any.add(domain) domains_if_any.add(domain)
# Filter out domains that don't have correct DNS, because then the CA # It's valid.
# won't be able to do DNS validation. problems[domain] = "The certificate is valid for at least another 30 days --- no need to replace."
def is_domain_dns_correct(domain):
# Must make qname absolute to prevent a fall-back lookup with a # Filter out domains that we can't provision a certificate for.
# search domain appended. def can_provision_for_domain(domain):
import dns.resolver # Let's Encrypt doesn't yet support IDNA domains.
try: # We store domains in IDNA (ASCII). To see if this domain is IDNA,
response = dns.resolver.query(domain + ".", "A") # we'll see if its IDNA-decoded form is different.
except: if idna.decode(domain.encode("ascii")) != domain:
problems[domain] = "Let's Encrypt does not yet support provisioning certificates for internationalized domains."
return False return False
return len(response) == 1 and str(response[0]) == env["PUBLIC_IP"]
domains = set(d for d in domains if is_domain_dns_correct(d)) # Does the domain resolve to this machine in public DNS? If not,
domains_if_any = set(d for d in domains_if_any if is_domain_dns_correct(d)) # we can't do domain control validation. For IPv6 is configured,
# make sure both IPv4 and IPv6 are correct because we don't know
# how Let's Encrypt will connect.
import dns.resolver
for rtype, value in [("A", env["PUBLIC_IP"]), ("AAAA", env.get("PUBLIC_IPV6"))]:
if not value: continue # IPv6 is not configured
try:
# Must make the qname absolute to prevent a fall-back lookup with a
# search domain appended, by adding a period to the end.
response = dns.resolver.query(domain + ".", rtype)
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
problems[domain] = "DNS isn't configured properly for this domain: DNS resolution failed (%s: %s)." % (rtype, str(e) or repr(e)) # NoAnswer's str is empty
return False
except Exception as e:
problems[domain] = "DNS isn't configured properly for this domain: DNS lookup had an error: %s." % str(e)
return False
if len(response) != 1 or str(response[0]) != value:
problems[domain] = "Domain control validation cannot be performed for this domain because DNS points the domain to another machine (%s %s)." % (rtype, ", ".join(str(r) for r in response))
return False
return True
domains = set(filter(can_provision_for_domain, domains))
# If there are any domains we definitely will provision for, add in # If there are any domains we definitely will provision for, add in
# additional domains to do at this time. # additional domains to do at this time.
if len(domains) > 0: if len(domains) > 0:
domains |= domains_if_any domains |= set(filter(can_provision_for_domain, domains_if_any))
# Sort, just to keep related domain names together in the next step. return (domains, problems)
domains = sort_domains(domains, env)
# Break into groups of up to 100 certificates at a time, which is Let's Encrypt's
# limit for a single certificate.
cert_domains = []
while len(domains) > 0:
cert_domains.append( domains[0:100] )
domains = domains[100:]
# Return a list of lists of domain names.
return cert_domains
def provision_certificates(env, agree_to_tos_url=None, logger=None): def provision_certificates(env, agree_to_tos_url=None, logger=None):
import requests.exceptions import requests.exceptions
@ -230,14 +246,25 @@ def provision_certificates(env, agree_to_tos_url=None, logger=None):
from free_tls_certificates import client from free_tls_certificates import client
# What domains to provision certificates for? This is a list of # What domains should we provision certificates for? And what
# lists of domains. # errors prevent provisioning for other domains.
certs = get_certificates_to_provision(env) domains, problems = get_certificates_to_provision(env)
if len(certs) == 0:
# Exit fast if there is nothing to do.
if len(domains) == 0:
return { return {
"requests": [], "requests": [],
"problems": problems,
} }
# Break into groups of up to 100 certificates at a time, which is Let's Encrypt's
# limit for a single certificate. We'll sort to put related domains together.
domains = sort_domains(domains, env)
certs = []
while len(domains) > 0:
certs.append( domains[0:100] )
domains = domains[100:]
# Prepare to provision. # Prepare to provision.
# Where should we put our Let's Encrypt account info and state cache. # Where should we put our Let's Encrypt account info and state cache.
@ -352,7 +379,8 @@ def provision_certificates(env, agree_to_tos_url=None, logger=None):
# Return what happened with each certificate request. # Return what happened with each certificate request.
return { return {
"requests": ret "requests": ret,
"problems": problems,
} }
def provision_certificates_cmdline(): def provision_certificates_cmdline():
@ -377,7 +405,14 @@ def provision_certificates_cmdline():
if not status["requests"]: if not status["requests"]:
# No domains need certificates. # No domains need certificates.
if "--headless" not in sys.argv or "-v" in sys.argv: if "--headless" not in sys.argv or "-v" in sys.argv:
print("No domains hosted on this box need a certificate at this time.") if len(status["problems"]) == 0:
print("No domains hosted on this box need a new TLS certificate at this time.")
elif len(status["problems"]) > 0:
print("No TLS certificates could be provisoned at this time:")
print()
for domain in sort_domains(status["problems"], env):
print("%s: %s" % (domain, status["problems"][domain]))
sys.exit(0) sys.exit(0)
# What happened? # What happened?
@ -459,6 +494,12 @@ Do you agree to the agreement? Type Y or N and press <ENTER>: """
# we're done for now. # we're done for now.
break break
# And finally show the domains with problems.
if len(status["problems"]) > 0:
print("TLS certificates could not be provisoned for:")
for domain in sort_domains(status["problems"], env):
print("%s: %s" % (domain, status["problems"][domain]))
# INSTALLING A NEW CERTIFICATE FROM THE CONTROL PANEL # INSTALLING A NEW CERTIFICATE FROM THE CONTROL PANEL
def create_csr(domain, ssl_key, country_code, env): def create_csr(domain, ssl_key, country_code, env):