Commit a27c1135 by shammash

Merge branch 'san' into 'master'

Add additional SANs support

See merge request !1
2 parents a88b153b 9e11ad5a
Pipeline #273 failed
in 1 minute 11 seconds
......@@ -190,11 +190,13 @@ class ACMEQueue(object):
return LDAPObj('cn=%s,%s' % (
escape_dn_chars(cn), self._root.dn))
def add_task(self, cn, initial_state='init'):
def add_task(self, cn, initial_state='init', san=None):
newobj = self._task_obj(cn)
newobj['objectClass'] = ['acmeRequest', 'top']
newobj['cn'] = cn
newobj['status'] = initial_state
if san is not None:
newobj['san'] = san
newobj['creationDate'] = datetime.utcnow().isoformat()
......@@ -245,8 +247,11 @@ class ACMECredentialsStore(object):
return self._fernet.decrypt(pkey)
def _all_names(cn):
def _all_names(ldapobj):
cn = ldapobj['cn']
names = [cn]
if 'san' in ldapobj:
# Only add "www" prefix to domain names that have one dot (rough
# approximation to first-level domains).
if not cn.startswith('www.') and cn.count('.') < 2:
......@@ -329,7 +334,7 @@ class ACMEStateMachine(object):
"""Verify ownership of the domain using the ACME protocol."""
# Save the challenge and the validation data for every alt-domain.
challenges = {}
for name in _all_names(ldapobj['cn']):
for name in _all_names(ldapobj):
auth = self._acme.request_domain_challenges(name) should be unnecessary now.
......@@ -347,7 +352,7 @@ class ACMEStateMachine(object):
def state_verify(self, ldapobj):
"""Verify that we are ready to answer the challenges for this domain."""
state = _State.deserialize(ldapobj['acmeState'])
for name in _all_names(ldapobj['cn']):
for name in _all_names(ldapobj):
auth = state.challenges[name].auth
response = state.challenges[name].response
challb = supported_challb(auth)
......@@ -365,7 +370,7 @@ class ACMEStateMachine(object):
def state_answer_challenge(self, ldapobj):
name = ldapobj['cn']
vhosts = _all_names(name)
vhosts = _all_names(ldapobj)
state = _State.deserialize(ldapobj['acmeState'])
# Answer all the domain-specific challenges.
......@@ -427,7 +432,7 @@ def _expiration_deadline(d):
return d - timedelta(EXPIRATION_SAFETY_MARGIN_DAYS, 0, 0)
def run_acme(queue, credstore, state_machine, all_domains, now=None):
def run_acme(queue, credstore, state_machine, all_domains, san_domains, now=None):
"""Run the ACME protocol state machine over pending tasks.
Performs a single iteration of the state machine for each pending
......@@ -445,9 +450,11 @@ def run_acme(queue, credstore, state_machine, all_domains, now=None):
credstore: an ACMECredentialsStore object
state_machine: an ACMEStateMachine object
all_domains: a list of domain names that should have SSL credentials
san_domains: a map domain name -> list of SANs to request
now: current time (for testing)
pending_task_domains = set()
for obj in queue.get_tasks():
......@@ -462,6 +469,7 @@ def run_acme(queue, credstore, state_machine, all_domains, now=None):
# Examine all known domains and figure out what needs to be done:
# new certificate, renewal, or nothing.
for cn in all_domains:
san = san_domains.get(cn, None)
# If there is already a pending task for this domain, do nothing.
if cn in pending_task_domains:
......@@ -470,14 +478,14 @@ def run_acme(queue, credstore, state_machine, all_domains, now=None):
creds = credstore.get_credentials(cn)
if not creds:'domain %s has no certificate, initializing...', cn)
queue.add_task(cn, 'init')
queue.add_task(cn, 'init', san)
# If the sslCredentials object does not have the sslCert attribute,
# but it isn't in the queue, re-initialize it.
elif not creds['sslCert']:'domain %s has incomplete setup, reinitializing...', cn)
queue.add_task(cn, 'init')
queue.add_task(cn, 'init', san)
deadline = _expiration_deadline(_parse_date(creds['expires']))
if deadline < now:'the certificate for domain %s is about to expire, renewing...', cn)
queue.add_task(cn, 'renew')
queue.add_task(cn, 'renew', san)
......@@ -27,6 +27,9 @@ DEFAULT_CONFIG = {
# Request these SANs
resolver = dns.resolver.Resolver(configure=False)
......@@ -82,7 +85,7 @@ def main():
execfile(LETSENCRYPT_CONFIG, {}, config)
logging.basicConfig(level=logging.DEBUG if opts.debug else logging.INFO)
q = letsencrypt.ACMEQueue()
credstore = letsencrypt.ACMECredentialsStore()
directory_url = letsencrypt.ACME_PROD_DIRECTORY_URL
......@@ -95,7 +98,14 @@ def main():
domains = itertools.chain(
config['INCLUDE_DOMAINS'], _all_domains(config['EXCLUDE_DOMAINS']))
letsencrypt.run_acme(q, credstore, sm, domains)
for domain, san in config['SAN_DOMAINS']:
if domain not in domains:
logging.error('SAN domain %s not configured', domain)
for s in san:
if s not in domains:
logging.error('SAN domain %s (for %s) not configured', s, domain)
letsencrypt.run_acme(q, credstore, sm, domains, config['SAN_DOMAINS'])
if __name__ == '__main__':
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!