def distribute(cm: cert.Certificate, ci: cert.CertInstance, state: cert.CertState): if opts.check_only: sld('Would distribute {}.'.format(ci.row_id)) return sli('Distributing {}:{}'. format(cm.name, ci.row_id)) cm_dict = {cm.name: cm} try: deployCerts(cm_dict, (ci,), allowed_states=(state,)) except Exception: sln('Skipping distribution of cert {} because {} [{}]'.format( cm.name, sys.exc_info()[0].__name__, str(sys.exc_info()[1])))
def consolidate_TLSA(cert_meta): """ Consolidate all TLSA RRs for one cert meta. This means TLSA include files are freshly created. @param cert_meta: Cert meta @type cert_meta: cert.Certificate instance @rtype: None @exceptions: """ prepublished_ci = None deployed_ci = None inst_list = cert_meta.active_instances # returns dict with state as key if not inst_list: return for state, ci in inst_list.items(): if state == CertState('prepublished'): if not prepublished_ci: prepublished_ci = ci else: sln('consolidate_TLSA: More than one instance of {} in state' ' "prepublished"'.format(cert_meta.name)) elif state == 'deployed': if not deployed_ci: deployed_ci = ci else: sln('consolidate_TLSA: More than one instance of {} in state' ' "deployed"'.format(cert_meta.name)) if not deployed_ci: sli('consolidate_TLSA: No instance of {} in state "deployed"'.format( cert_meta.name)) return prepublished_TLSA = {} if prepublished_ci: prepublished_TLSA = cert_meta.TLSA_hash(prepublished_ci) deployed_TLSA = cert_meta.TLSA_hash(deployed_ci) distribute_tlsa_rrs( cert_meta, tuple(deployed_TLSA.values()) + tuple(prepublished_TLSA.values()))
def ssh_connection(dest_host): """ Open a ssh connection. @param dest_host: fqdn of target host @type dest_host: string @rtype: paramiko.SSHClient (connected transport) @exceptions: If unable to connect """ client = SSHClient() client.load_host_keys(expanduser('~/.ssh/known_hosts')) sld('Connecting to {}'.format(dest_host)) try: client.connect(dest_host, username=Misc.SSH_CLIENT_USER_NAME) except Exception: sln('Failed to connect to host {}, because {} [{}]'.format( dest_host, sys.exc_info()[0].__name__, str(sys.exc_info()[1]))) raise else: sld('Connected to host {}'.format(dest_host)) return client
def _get_intermediate_instance(db: db_conn, int_cert: x509.Certificate) -> CertInstance: """ Return CertInstance of intermediate CA cert or create a new CertInstance if not found :param db: Opened DB connection :param int_cert: the CA cert to find the ci for :return: ci of CA cert """ ci = CertKeyStore.ci_from_cert_and_name(db=db, cert=int_cert, name=Misc.SUBJECT_LE_CA) if ci: return ci sln('Storing new intermediate cert.') # intermediate is not in DB - insert it # obtain our cert meta - check, if it exists if Misc.SUBJECT_LE_CA in Certificate.names(db): cm = Certificate.create_or_load_cert_meta( db, Misc.SUBJECT_LE_CA) # yes: we have meta but no instance sln('Cert meta for intermediate cert exists, but no instance.') else: # no: this ist 1st cert with this CA sln('Cert meta for intermediate does not exist, creating {}.'.format( Misc.SUBJECT_LE_CA)) cm = create_CAcert_meta(db=db, name=Misc.SUBJECT_LE_CA, cert_type=CertType('LE')) ci = cm.create_instance(state=CertState('issued'), not_before=int_cert.not_valid_before, not_after=int_cert.not_valid_after) cm.save_instance(ci) ci.store_cert_key(algo=EncAlgoCKS('rsa'), cert=int_cert, key=b'') ##FIXME## might be ec in the future cm.save_instance(ci) return ci
def scheduleCerts(db: db_conn, cert_metas: Dict[str, cert.Certificate]) -> None: """ Schedule state transitions and do related actions of CertInstances :param db: Open Database connection :param cert_metas: list of Cerificate instances to act on :return: """ global ps_delete, to_be_deleted (DBAccount, Misc, Pathes, X509atts) = get_config() opts = get_options() def issue(cm: cert.Certificate) -> Optional[cert.CertInstance]: """ If cert type is 'LE', issue a Letsencrypt cert :param cm: cert meta :return: ci of new cert or None """ if cm.cert_type == cert.CertType('local'): return None if opts.check_only: sld('Would issue {}.'.format(cm.name)) return if not cm.disabled: sli('Requesting issue from LE for {}'.format(cm.name)) return issue_LE_cert(cm) def prepublish(cm: cert.Certificate, active_ci: cert.CertInstance, new_ci: cert.CertInstance) -> None: """ Prepublish cert hashes per TLSA RRs in DNS :param cm: Our cert meta data instance :param active_ci: CertInstance currently in use :param new_ci: CertInstance just created but not yet deployed :return: """ if opts.check_only: sld('Would prepublish {} {}.'.format(active_ci.row_id, new_ci.row_id)) return # collect hashes for all certs in all algos hashes = tuple(cm.TLSA_hashes(active_ci).values()) + tuple(cm.TLSA_hashes(new_ci).values()) sli('Prepublishing {}:{}:{}'. format(cm.name, active_ci.row_id, new_ci.row_id)) distribute_tlsa_rrs(cm, hashes) new_ci.state = cert.CertState('prepublished') cm.save_instance(new_ci) def distribute(cm: cert.Certificate, ci: cert.CertInstance, state: cert.CertState): if opts.check_only: sld('Would distribute {}.'.format(ci.row_id)) return sli('Distributing {}:{}'. format(cm.name, ci.row_id)) cm_dict = {cm.name: cm} try: deployCerts(cm_dict, (ci,), allowed_states=(state,)) except Exception: sln('Skipping distribution of cert {} because {} [{}]'.format( cm.name, sys.exc_info()[0].__name__, str(sys.exc_info()[1]))) def expire(cm, ci): if opts.check_only: sld('Would expire {}.'.format(ci.row_id)) return sli('State transition from {} to EXPIRED of {}:{}'. format(ci.state, cm.name, ci.row_id)) ci.state = cert.CertState('expired') cm.save_instance(ci) def archive(cm, ci): if opts.check_only: sld('Would archive {}.'.format(ci.row_id)) return sli('State transition from {} to ARCHIVED of {}:{}'. format(ci.state, cm.name, ci.row_id)) ci.state = cert.CertState('archived') cm.save_instance(ci) for cm in cert_metas.values(): sld('{} {} ------------------------------'.format( cm.name, 'DISABLED' if cm.disabled else '')) if cm.subject_type in (cert.SubjectType('CA'),cert.SubjectType('reserved')): continue issued_ci = None prepublished_ci = None deployed_ci = None surviving = _find_to_be_deleted(cm) if not surviving: ci = issue(cm) if ci: distribute(cm, ci, cert.CertState('issued')) continue for ci in surviving: if ci.state == cert.CertState('expired'): archive(cm, ci) continue if datetime.utcnow() >= (ci.not_after + timedelta(days=1)): if ci.state != cert.CertState('deployed'): expire(cm, ci) continue elif ci.state == cert.CertState('issued'): issued_ci = ci elif ci.state == cert.CertState('prepublished'): prepublished_ci = ci elif ci.state == cert.CertState('deployed'): deployed_ci = ci else: assert (ci.state in (cert.CertState('issued'), cert.CertState('prepublished'), cert.CertState('deployed'),)) if deployed_ci and issued_ci: # issued too old to replace deployed in future? if issued_ci.not_after < (deployed_ci.not_after + timedelta(days=Misc.LOCAL_ISSUE_MAIL_TIMEDELTA)): to_be_deleted |= set((issued_ci,)) # yes: mark for delete issued_ci = None # request issue_mail if near to expiration if (deployed_ci and cm.cert_type == 'local' and not cm.authorized_until and datetime.utcnow() >= (deployed_ci.not_after - timedelta(days=Misc.LOCAL_ISSUE_MAIL_TIMEDELTA))): to_be_mailed.append(cm) sld('schedule.to_be_mailed: ' + str(cm)) if cm.disabled: continue # deployed cert expired or no cert deployed? if (not deployed_ci) or \ (datetime.utcnow() >= deployed_ci.not_after - timedelta(days=1)): distributed = False sld('scheduleCerts: no deployed cert or deployed cert' 'expired {}'.format(str(deployed_ci))) if prepublished_ci: # yes - distribute prepublished distribute(cm, prepublished_ci, cert.CertState('prepublished')) distributed = True elif issued_ci: # or issued cert? distribute(cm, issued_ci, cert.CertState('issued')) # yes - distribute it distributed = True if deployed_ci: expire(cm, deployed_ci) # and expire deployed cert if not distributed: ci = issue(cm) if ci: distribute(cm, ci, cert.CertState('issued')) continue if cm.cert_type == 'local': continue # no TLSAs with local certs # We have an active LE cert deployed if datetime.utcnow() >= \ (deployed_ci.not_after - timedelta(days=Misc.PRE_PUBLISH_TIMEDELTA)): # pre-publishtime reached? ci = issued_ci if prepublished_ci: # yes: TLSA already pre-published? continue # yes elif not issued_ci: # do we have a cert handy? ci = issue(cm) # no: create one if not ci: sln('Failed to issue cert for prepublishing of {}'.format(cm.name)) continue sld('scheduleCerts will call prepublish with deployed_ci={}, ci={}'.format( str(deployed_ci), str(ci))) prepublish(cm, deployed_ci, ci) # and prepublish it # end for name in cert_names if opts.check_only: sld('Would delete and mail..') return for ci in to_be_deleted: sld('Deleting {}'.format(ci.row_id)) result = ci.cm.delete_instance(ci) if result != 1: sln('Failed to delete cert instance {}'.format(ci.row_id)) if to_be_mailed: body = str('Following local Certificates must be issued prior to {}:\n'. format(date.today() + timedelta(days=Misc.LOCAL_ISSUE_MAIL_TIMEDELTA))) for cert_meta in to_be_mailed: body += str('\t{} \t{}'.format(cert_meta.name, '[DISABLED]' if cert_meta.disabled else '')) cert_meta.update_authorized_until(datetime.utcnow()) msg = MIMEText(body) msg['Subject'] = 'Local certificate issue reminder' msg['From'] = Misc.MAIL_SENDER msg['To'] = Misc.MAIL_RECIPIENT s = smtplib.SMTP(Misc.MAIL_RELAY) s.send_message(msg) s.quit()
def create_local_ca_cert( db: db_conn, cm: Certificate ) -> Tuple[x509.Certificate, rsa.RSAPrivateKeyWithSerialization, CertInstance]: """ Create a new local CA cert (use an existing one, if one in db) Make it available in globals local_cacert, local_cakey and local_cacert_instance :param db: Opened DB connection :param cm: CA cert Certifcate meta instance :return: Tuple of cacert, cakey and cacert instance and globals local_cacert, local_cakey, local_cacert_instance setup """ global local_cacert, local_cakey, local_cacert_instance # Read pass phrase from user match = False while not match: sli('Please enter passphrase for new CA cert (ASCII only).') try: pp1 = getpass.getpass(prompt='passphrase: ') except UnicodeDecodeError: sle('None-ASCII character found.') continue if pp1 == '': sln('Passphrases must not be empty.') continue sli('Please enter it again.') try: pp2 = getpass.getpass(prompt='passphrase: ') except UnicodeDecodeError: pass if pp1 == pp2: match = True else: sln('Both passphrases differ.') # Generate our key cakey = rsa.generate_private_key(public_exponent=65537, key_size=Misc.LOCAL_CA_BITS, backend=default_backend()) # compose cert # Various details about who we are. For a self-signed certificate the # subject and issuer are always the same. serial = randbits(32) name_dict = X509atts.names subject = issuer = x509.Name([ x509.NameAttribute(x509.NameOID.COUNTRY_NAME, name_dict['C']), x509.NameAttribute(x509.NameOID.LOCALITY_NAME, name_dict['L']), x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, name_dict['O']), x509.NameAttribute(x509.NameOID.COMMON_NAME, name_dict['CN']), ]) not_after = datetime.datetime.utcnow() + datetime.timedelta( days=Misc.LOCAL_CA_LIFETIME) not_before = datetime.datetime.utcnow() - datetime.timedelta(days=1) ci = cm.create_instance(not_before=not_before, not_after=not_after, state=CertState('issued')) ski = x509.SubjectKeyIdentifier.from_public_key(cakey.public_key()) cacert = x509.CertificateBuilder().subject_name(subject).issuer_name( issuer).public_key(cakey.public_key()).serial_number( serial).not_valid_before(not_before).not_valid_after( # Our certificate will be valid for 10 days not_after).add_extension( # CA and no intermediate CAs x509.BasicConstraints(ca=True, path_length=0), critical=True).add_extension( ski, critical=False, # Sign our certificate with our private key ).add_extension(x509.KeyUsage(digital_signature=True, key_cert_sign=True, crl_sign=True, key_encipherment=False, content_commitment=False, data_encipherment=False, key_agreement=False, encipher_only=False, decipher_only=False), critical=True).sign( cakey, hashes.SHA512(), default_backend()) sli('CA cert serial {} with {} bit key, valid until {} created.'.format( serial, Misc.LOCAL_CA_BITS, not_after.isoformat())) ci.store_cert_key(algo='rsa', cert=cacert, key=cakey) cm.save_instance(ci) # CA cert loaded, cache it and return cache_cacert(cacert, cakey, ci) return (cacert, cakey, ci)
def get_cacert_and_key( db: db_conn ) -> Tuple[x509.Certificate, rsa.RSAPrivateKeyWithSerialization, CertInstance]: """ Return a valid local CA certificate and a loaded private key. Use globals local_cacert, local_cakey and local_cacert_instance if available If necessary, create a local CAcert or read a historical one from disk. Store it in DB, creating necessary rows in Subjects, Certificates and Certinstances and store them in LocalCaCertCache Does exit(1) if CA key could not be loaded. :param db: Opened DB connection :return: Tuple of cacert, cakey and cacert instance and globals local_cacert, local_cakey, local_cacert_instance setup """ if LocalCaCertCache.cert and LocalCaCertCache.key and LocalCaCertCache.ci: return (LocalCaCertCache.cert, LocalCaCertCache.key, LocalCaCertCache.ci) cacert = None cakey = None ci = None cm = Certificate.create_or_load_cert_meta(db, name=Misc.SUBJECT_LOCAL_CA) if cm.in_db: # loaded from DB? ci = cm.most_recent_active_instance # YES: load its active CI if ci: # one there, which is valid today? cks = list(ci.the_cert_key_stores.values())[ 0] # expecting only one algorithm cacert_pem = cks.cert_for_ca cakey_pem = cks.key_for_ca algo = cks.algo cacert = x509.load_pem_x509_certificate(data=cacert_pem, backend=default_backend()) cakey = _load_cakey(cakey_pem) if not cakey: # operator aborted pass phrase input sle('Can' 't create certificates without passphrase') exit(1) # CA cert loaded, cache it and return cache_cacert(cacert, cakey, ci) return (cacert, cakey, ci) sln('Missed active cacert instance or store in db, where it should be: {}' .format(cm.name)) else: # CM not in DB, create rows in Subjects and Certificates for it cm = create_CAcert_meta(db=db, name=Misc.SUBJECT_LOCAL_CA, cert_type=CertType('local')) # Try to load active historical CA cert from flat file result = load_historical_cert_from_flatfile(cm) if result: # loaded, cached and stored in DB return result # globals already setup else: # none found in flat file - issue a new one result = create_local_ca_cert(db, cm) if result: # issued, cached and stored in DB return result # globals already setup else: sle('Failed to issue CA cert') sys.exit(1) if not ci: # no most_recent_active_instance found above, issue one result = create_local_ca_cert(db, cm) if result: # issued, cached and stored in DB return result else: sle('Failed to issue CA cert') sys.exit(1)
def _authorize(cert_meta: Certificate, account: Account) -> Optional[Order]: """ Try to prove the control about a DNS object. @param cert_meta: Cert meta instance to issue an certificate for @type cert_meta: Cert meta instance @param account: Our letsencrypt account @type account: manuale_cli.load_account instance @rtype: True if all fqdns could be authorized, False otherwise @exceptions: manuale_errors.AutomatoesError on Network or other fatal error """ acme = AcmeV2(Misc.LE_SERVER, account) FQDNS = dict() FQDNS[cert_meta.name] = 0 for name in cert_meta.altnames: if name not in FQDNS: FQDNS[name] = 0 domains = list(FQDNS.keys()) try: order: Order = acme.new_order(domains, 'dns') except AcmeError as e: print(e) return None returned_order = acme.query_order(order) sld('new_order for {} returned\n{}'.format(cert_meta.name, print_order(returned_order))) if order.expired or order.invalid: sle("{}: Order is {} {}. Giving up.".format( cert_meta.name, 'invalid' if order.invalid else '', 'expired' if order.expired else '')) return None returned_fqdns = [idf['value'] for idf in order.contents['identifiers']] if set(domains) != set(returned_fqdns): sle("{}: List of FQDNS returned by order does not match ordered FQDNs:\n{}\n{}" .format(cert_meta.name, returned_fqdns, domains)) return None if order.contents['status'] != 'pending': return order # all done, if now challenge pending fqdn_challenges = {} # key = domain, value = chllenge pending_challenges = acme.get_order_challenges(order) for challenge in pending_challenges: if challenge.status == 'valid': sli(" {} is already authorized until {}.".format( challenge.domain, challenge.expires)) continue fqdn_challenges[challenge.domain] = challenge # find zones by fqdn zones = {} sld('Calling zone_and_FQDN_from_altnames()') for (zone, fqdn) in Certificate.zone_and_FQDN_from_altnames(cert_meta): if fqdn in fqdn_challenges: if zone in zones: if fqdn not in zones[zone]: zones[zone].append(fqdn) else: zones[zone] = [fqdn] sld('zones: {}'.format(zones)) if not fqdn_challenges: server_order = acme.query_order(order) order.contents = server_order.contents sli("All Altnames of {} are already authorized.Order status = {}". format(cert_meta.name, order.contents['status'])) return order create_challenge_responses_in_dns(zones, fqdn_challenges) sld('{} completed DNS setup on hidden primary for all pending FQDNs'. format(datetime.datetime.utcnow().isoformat())) # Validate challenges authorized_until = None sli('Waiting 15 seconds for dns propagation') time.sleep(15) for challenge in pending_challenges: # wait maximum 2 minutes sld('{} starting verification of {}'.format( datetime.datetime.utcnow().isoformat(), challenge.domain)) response = acme.verify_order_challenge(challenge, timeout=5, retry_limit=5) sld('{} acme.verify_order_challenge returned "{}"'.format( datetime.datetime.utcnow().isoformat(), response['status'])) if response['status'] == "valid": sld("{}: OK! Authorization lasts until {}.".format( challenge.domain, challenge.expires)) authorized_until = challenge.expires elif response['status'] == 'invalid': sle("{}: Challenge failed, because: {} ({})".format( challenge.domain, response['error']['detail'], response['error']['type'])) # we need either all challenges or none: repeat with next cron cacle return None else: sln("{}: Challenge returned status {}".format( challenge.domain, response['status'])) # we need either all challenges or none: repeat with next cron cacle return None server_order = acme.query_order(order) order.contents = server_order.contents sld("All Altnames of {} authorized.Order status = {}".format( cert_meta.name, order.contents['status'])) # remember new expiration date in DB if authorized_until: updates = cert_meta.update_authorized_until( datetime.datetime.fromisoformat(re.sub('Z', '', authorized_until))) if updates != 1: sln('Failed to update DB with new authorized_until timestamp') delete_challenge_responses_in_dns(zones) sli("FQDNs authorized. Let's Encrypt!") return order
def distribute_cert(fd, dest_host, dest_dir, file_name, place, jail): """ Distribute cert and key to a host, jail (if any) and place. Optional reload the service. If global opts.extract set, instead of distributing to a host, certificat and key are written to the local work directory. @param fd: file descriptor of memory stream @type fd: io.StringIO @param dest_host: fqdn of target host @type dest_host: string @param dest_dir: target directory @type dest_dir: string @param file_name: file name of key or cert file @type file_name: string @param place: place with details about setting mode and uid/gid of file @type place: serverPKI.cert.Place instance @param jail: name of jail for service to reload @type jail: string or None @rtype: not yet any @exceptions: IOError """ sld('Handling dest_host {} and dest_dir "{}" in distribute_cert'.format( dest_host, dest_dir)) with ssh_connection(dest_host) as client: with client.open_sftp() as sftp: try: sftp.chdir(str(dest_dir)) except IOError: sln('{}:{} does not exist - creating\n\t{}'.format( dest_host, dest_dir, sys.exc_info()[0].__name__)) try: sftp.mkdir(str(dest_dir)) except IOError: sle('Cant create {}:{}: Missing parent?\n\t{}'.format( dest_host, dest_dir, sys.exc_info()[0].__name__, str(sys.exc_info()[1]))) raise sftp.chdir(str(dest_dir)) sli('{} => {}:{}'.format(file_name, dest_host, dest_dir)) fat = sftp.putfo(fd, file_name, confirm=True) sld('size={}, uid={}, gid={}, mtime={}'.format( fat.st_size, fat.st_uid, fat.st_gid, fat.st_mtime)) if 'key' in file_name: sld('Setting mode to 0o400 of {}:{}/{}'.format( dest_host, dest_dir, file_name)) mode = 0o400 if place.mode: mode = place.mode sld('Setting mode of key at target to {}'.format( oct(place.mode))) sftp.chmod(file_name, mode) if place.pgLink: try: sftp.unlink('postgresql.key') except IOError: pass # none exists: ignore sftp.symlink(file_name, 'postgresql.key') sld('{} => postgresql.key'.format(file_name)) if 'key' in file_name or place.chownBoth: uid = gid = 0 if place.uid: uid = place.uid if place.gid: gid = place.gid if uid != 0 or gid != 0: sld('Setting uid/gid to {}:{} of {}:{}/{}'.format( uid, gid, dest_host, dest_dir, file_name)) sftp.chown(file_name, uid, gid) elif place.pgLink: try: sftp.unlink('postgresql.crt') except IOError: pass # none exists: ignore sftp.symlink(file_name, 'postgresql.crt') sld('{} => postgresql.crt'.format(file_name)) if jail and place.reload_command: try: cmd = str((place.reload_command).format(jail)) except: #No "{}" in reload command: means no jail cmd = place.reload_command sli('Executing "{}" on host {}'.format(cmd, dest_host)) with client.get_transport().open_session() as chan: chan.settimeout(10.0) chan.set_combine_stderr(True) chan.exec_command(cmd) remote_result_msg = '' timed_out = False while not chan.exit_status_ready(): if timed_out: break if chan.recv_ready(): try: data = chan.recv(1024) except (timeout): sle('Timeout on remote execution of "{}" on host {}' .format(cmd, dest_host)) break while data: remote_result_msg += (data.decode('ascii')) try: data = chan.recv(1024) except (timeout): sle('Timeout on remote execution of "{}" on host {}' .format(cmd, dest_host)) tmp = timed_out timed_out = True break es = int(chan.recv_exit_status()) if es != 0: sln('Remote execution failure of "{}" on host {}\texit={}, because:\n\r{}' .format(cmd, dest_host, es, remote_result_msg)) else: sli(remote_result_msg)
def deployCerts( cert_metas: Dict[str, Certificate], cert_instances: Optional[Tuple[CertInstance]] = None, allowed_states: Tuple[CertState] = (CertState('issued'), ) ) -> bool: """ Deploy a list of (certificates. keys and TLSA RRs, using paramiko/sftp) and dyn DNS (or zone files). Restart service at target host and reload nameserver (if using zone files). :param cert_metas: Dict of cert metas, telling which certs to deploy, key is cert subject name :param cert_instances: Optional list of CertInstance instances :param allowed_states: States describing CertInstance states to act on :return: True if successfully deployed certs """ error_found = False limit_hosts = False opts = get_options() only_host = [] if opts.only_host: only_host = opts.only_host if len(only_host) > 0: limit_hosts = True skip_host = [] if opts.skip_host: skip_host = opts.skip_host sld('limit_hosts={}, only_host={}, skip_host={}'.format( limit_hosts, only_host, skip_host)) for cert_meta in cert_metas.values(): if len(cert_meta.disthosts) == 0: continue the_instances = [] hashes = [] ##FIXME## highly speculative! insts = cert_instances if cert_instances else [ y for (x, y) in cert_meta.active_instances.items() ] for ci in insts: if ci.state in allowed_states: the_instances.append(ci) if len(the_instances) == 0: etxt = 'No valid cerificate for {} in DB - create it first\n' \ 'States being considered are {}'. \ format(cert_meta.name, [state for state in allowed_states]) sli(etxt) if cert_instances: # let caller handle this error, if we have explicit inst ids raise MyException(etxt) else: continue # more than 1 member of the_instances only expected with cert.instance(i).encryption_algo == 'both' for ci in the_instances: state = ci.state cacert_text = cert_meta.cacert_PEM(ci) host_omitted = False cksd = ci.the_cert_key_stores for encryption_algo in cksd.keys(): cks = cksd[encryption_algo] cert_text = cks.cert key_text = cks.key TLSA_text = cks.hash hashes.append(TLSA_text) for fqdn, dh in cert_meta.disthosts.items(): if fqdn in skip_host: host_omitted = True continue if limit_hosts and (fqdn not in only_host): host_omitted = True continue dest_path = PurePath('/') sld('{}: {}'.format(cert_meta.name, fqdn)) for jail in (dh['jails'].keys() or ('', )): # jail is empty if no jails if '/' in jail: sle('"/" in jail name "{}" not allowed with subject {}.' .format(jail, cert_meta.name)) error_found = True return False jailroot = dh[ 'jailroot'] if jail != '' else '' # may also be empty dest_path = PurePath('/', jailroot, jail) sld('{}: {}: {}'.format(cert_meta.name, fqdn, dest_path)) the_jail = dh['jails'][jail] if len(the_jail['places']) == 0: sle('{} subject has no place attribute.'.format( cert_meta.name)) error_found = True return False for place in the_jail['places'].values(): sld('Handling jail "{}" and place {}'.format( jail, place.name)) fd_key = StringIO(key_text) fd_cert = StringIO(cert_text) key_file_name = key_name(cert_meta.name, cert_meta.subject_type, encryption_algo) cert_file_name = cert_name(cert_meta.name, cert_meta.subject_type, encryption_algo) pcp = place.cert_path if '{}' in pcp: # we have a home directory named like the subject pcp = pcp.format(cert_meta.name) # make sure pcb does not start with '/', which would ignore dest_path: if PurePath(pcp).is_absolute(): dest_dir = PurePath( dest_path, PurePath(pcp).relative_to('/')) else: dest_dir = PurePath(dest_path, PurePath(pcp)) sld('Handling fqdn {} and dest_dir "{}" in deployCerts' .format(fqdn, dest_dir)) try: if place.key_path: key_dest_dir = PurePath( dest_path, place.key_path) distribute_cert(fd_key, fqdn, key_dest_dir, key_file_name, place, None) elif place.cert_file_type == 'separate': distribute_cert(fd_key, fqdn, dest_dir, key_file_name, place, None) if cert_meta.cert_type == 'LE': chain_file_name = cert_cacert_chain_name( cert_meta.name, cert_meta.subject_type, encryption_algo) fd_chain = StringIO(cert_text + cacert_text) distribute_cert( fd_chain, fqdn, dest_dir, chain_file_name, place, jail) elif place.cert_file_type == 'combine key': cert_file_name = key_cert_name( cert_meta.name, cert_meta.subject_type, encryption_algo) fd_cert = StringIO(key_text + cert_text) if cert_meta.cert_type == 'LE': chain_file_name = key_cert_cacert_chain_name( cert_meta.name, cert_meta.subject_type, encryption_algo) fd_chain = StringIO(key_text + cert_text + cacert_text) distribute_cert( fd_chain, fqdn, dest_dir, chain_file_name, place, jail) elif place.cert_file_type == 'combine both': cert_file_name = key_cert_cacert_name( cert_meta.name, cert_meta.subject_type, encryption_algo) fd_cert = StringIO(key_text + cert_text + cacert_text) elif place.cert_file_type == 'combine cacert': cert_file_name = cert_cacert_name( cert_meta.name, cert_meta.subject_type, encryption_algo) fd_cert = StringIO(cert_text + cacert_text) distribute_cert(fd_key, fqdn, dest_dir, key_file_name, place, None) # this may be redundant in case of LE, where the cert was in chained file distribute_cert(fd_cert, fqdn, dest_dir, cert_file_name, place, jail) except IOError: # distribute_cert may error out error_found = True break # no cert - no TLSA sli('') if opts.sync_disk: # skip TLSA stuff if doing consolidate continue if not opts.no_TLSA: distribute_tlsa_rrs(cert_meta, hashes) if not host_omitted and not cert_meta.subject_type == 'CA': ci.state = CertState('deployed') cert_meta.save_instance(ci) else: sln('State of cert {} not promoted to DEPLOYED, ' 'because hosts where limited or skipped'.format( cert_meta.name)) # clear mail-sent-time if local cert. if cert_meta.cert_type == CertType('local'): cert_meta.update_authorized_until(None) updateSOAofUpdatedZones() return not error_found