Esempio n. 1
0
 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])))
Esempio n. 2
0
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()))
Esempio n. 3
0
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
Esempio n. 4
0
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
Esempio n. 5
0
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()
Esempio n. 6
0
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)
Esempio n. 7
0
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)
Esempio n. 8
0
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
Esempio n. 9
0
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)
Esempio n. 10
0
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