Example #1
0
def consolidate_cert(cert_meta: Certificate):
    """
    Consolidate cert targets of one cert meta.
    This means cert and key files of instance in state "deployed"
    are redistributed.
    
    @param cert_meta:   Cert meta
    @type cert_meta:    cert.Certificate
    @rtype:             None
    @exceptions:
    """
    deployed_ci = None

    inst_dict = cert_meta.active_instances
    sld('consolidate_cert: inst_list = {}'.format(inst_dict))
    if not inst_dict: return

    for state, ci in inst_dict.items():
        if state == CertState('deployed'):
            deployed_ci = ci
            break

    if not deployed_ci:
        sli('consolidate_cert: No instance of {} in state "deployed"'.format(
            cert_meta.name))
        return

    try:
        deployCerts({cert_meta.name: cert_meta},
                    cert_instances=(deployed_ci, ),
                    allowed_states=(CertState('deployed'), ))
    except MyException:
        pass
    return
Example #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()))
Example #3
0
def load_historical_cert_from_flatfile(
    cm: Certificate
) -> Optional[Tuple[x509.Certificate, rsa.RSAPrivateKeyWithSerialization,
                    CertInstance]]:
    """
    Load a historical CA cert from flat file
    Make it available in globals local_cacert, local_cakey and local_cacert_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

    # do we have a historical CA cert on disk?
    if Path(Pathes.ca_cert).exists() and Path(Pathes.ca_key).exists:
        sli('Using CA key at {}'.format(Pathes.ca_key))  # yes
        sli('Will be stored in DB')
        with open(Pathes.ca_cert, 'rb') as f:
            cacert_pem = f.read()
        cacert = x509.load_pem_x509_certificate(cacert_pem, default_backend())

        if not (cacert.not_valid_before < datetime.datetime.now()
                and cacert.not_valid_after > datetime.datetime.now()):

            sli('Historical cert on flatfile is outdated')
            return None
        else:
            with open(Pathes.ca_key, 'rb') as f:
                cakey_pem = f.read()
            cakey = _load_cakey(cakey_pem)
            ci = cm.create_instance(not_before=cacert.not_valid_before,
                                    not_after=cacert.not_valid_after,
                                    state=CertState('issued'))
            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)
    else:
        return None
Example #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
Example #5
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)
Example #6
0
def issue_local_cert(cert_meta: Certificate) -> Optional[CertInstance]:
    """
    Ask local CA to issue a certificate.
    Will ask for a passphrase to access the CA key.
    On success, inserts a row into CertInstances.
    If this is the first local instance, additional rows are inserted
    into Subjects, Certificates and CertInstances for local CA cert.
    Additional Certinstances may also be inserted if the local CA cert
    changes.
    FIXME: Currently only supports rsa keys

    :param cert_meta:   Cert meta instance to issue an certificate for
    :rtype:             cert instance id in DB of new cert or None
    """

    cacert, cakey, cacert_ci = get_cacert_and_key(cert_meta.db)

    sli('Creating key ({} bits) and cert for {} {}. Using CA cert {}'.format(
        int(X509atts.bits), cert_meta.subject_type, cert_meta.name,
        cacert_ci.row_id))
    serial = x509.random_serial_number()
    # Generate our key
    key = rsa.generate_private_key(public_exponent=65537,
                                   key_size=int(X509atts.bits),
                                   backend=default_backend())

    builder = x509.CertificateBuilder()
    builder = builder.subject_name(
        x509.Name([
            x509.NameAttribute(NameOID.COMMON_NAME, cert_meta.name),
        ]))
    builder = builder.issuer_name(x509.Name(cacert.subject, ))

    not_valid_before = datetime.datetime.utcnow() - datetime.timedelta(days=1)
    not_valid_after = datetime.datetime.utcnow() + datetime.timedelta(
        days=X509atts.lifetime)
    builder = builder.not_valid_before(not_valid_before)
    builder = builder.not_valid_after(not_valid_after)
    builder = builder.serial_number(serial)

    public_key = key.public_key()
    builder = builder.public_key(public_key)

    builder = builder.add_extension(
        x509.BasicConstraints(ca=False, path_length=None),
        critical=True,
    )

    ski = x509.SubjectKeyIdentifier.from_public_key(key.public_key())
    builder = builder.add_extension(
        ski,
        critical=False,
    )

    try:
        builder = builder.add_extension(
            x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
                x509.SubjectKeyIdentifier.from_public_key(cakey.public_key())),
            critical=False,
        )
    except x509.extensions.ExtensionNotFound:
        sle('Could not add a AuthorityKeyIdentifier, because CA has no SubjectKeyIdentifier'
            )

    if cert_meta.subject_type == 'client':
        alt_names = [
            x509.RFC822Name(cert_meta.name),
        ]
    else:
        alt_names = [
            x509.DNSName(cert_meta.name),
        ]
    for n in cert_meta.altnames:
        if cert_meta.subject_type == 'client':
            alt_names.append(x509.RFC822Name(n))
        else:
            alt_names.append(x509.DNSName(n))
    builder = builder.add_extension(
        x509.SubjectAlternativeName(alt_names),
        critical=False,
    )
    builder = builder.add_extension(
        x509.KeyUsage(digital_signature=True,
                      key_encipherment=True
                      if cert_meta.subject_type == 'server' else False,
                      content_commitment=False,
                      data_encipherment=False,
                      key_agreement=False,
                      key_cert_sign=False,
                      crl_sign=False,
                      encipher_only=False,
                      decipher_only=False),
        critical=True,
    )

    eku = None
    if cert_meta.subject_type == 'server':
        eku = x509.oid.ExtendedKeyUsageOID.SERVER_AUTH
    elif cert_meta.subject_type == 'client':
        eku = x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH
    if eku:
        builder = builder.add_extension(
            x509.ExtendedKeyUsage((eku, )),
            critical=True,
        )

    cert = builder.sign(private_key=cakey,
                        algorithm=hashes.SHA384(),
                        backend=default_backend())
    ci = cert_meta.create_instance(state=CertState('issued'),
                                   not_before=not_valid_before,
                                   not_after=not_valid_after,
                                   ca_cert_ci=cacert_ci)
    ci.store_cert_key(algo=cert_meta.encryption_algo, cert=cert, key=key)
    cert_meta.save_instance(ci)

    sli('Certificate for {} {}, serial {}, valid until {} created.'.format(
        cert_meta.subject_type, cert_meta.name, serial,
        not_valid_after.isoformat()))

    return ci
Example #7
0
def issue_LE_cert(cert_meta: Certificate) -> Optional[CertInstance]:
    """
    Try to issue a Letsencrypt certificate.
    Does authorization if necessary.
    On success, inserts a row into CertInstances.
    If this is the first Letsencrypt instance, additional rows are inserted
    into Subjects, Certificates and CertInstances for LE intermediate cert.
    Additional Certinstances may also be inserted if the intermediate cert
    from LE  changes.
    :param cert_meta: Description of certificate to issue
    :return: Instance of new cert
    """

    # Set up logging
    root = logging.getLogger('automatoes')
    root.setLevel(logging.INFO)
    handler = logging.StreamHandler(sys.stderr)
    handler.setFormatter(logging.Formatter("%(message)s"))
    root.addHandler(handler)

    os.chdir(str(Pathes.work))  ##FIXME## remove this ?

    sli('Creating certificate for {} and crypto algo {}'.format(
        cert_meta.name, cert_meta.encryption_algo))

    try:
        account: Account = manuale_cli.load_account(str(Pathes.le_account))
    except:
        sle('Problem with Lets Encrypt account data at {}'.format(
            Pathes.le_account))
        return None

    if cert_meta.encryption_algo == EncAlgo('rsa plus ec'):
        encryption_algos = (EncAlgoCKS('rsa'), EncAlgoCKS('ec'))
    else:
        encryption_algos = (cert_meta.encryption_algo, )

    results = []

    for encryption_algo in encryption_algos:

        result = _issue_cert_for_one_algo(encryption_algo, cert_meta, account)
        if not result:
            return None
        else:
            results.append(result)

    # loop ensures to store either all cks in DB or none:
    ci = None
    for result in results:

        if not ci:
            cacert_ci = _get_intermediate_instance(
                db=cert_meta.db, int_cert=result['Intermediate'])
            ci = cert_meta.create_instance(
                state=CertState('issued'),
                not_before=result['Cert'].not_valid_before,
                not_after=result['Cert'].not_valid_after,
                ca_cert_ci=cacert_ci)
        cks = ci.store_cert_key(algo=result['Algo'],
                                cert=result['Cert'],
                                key=result['Key'])

        sli('Certificate issued for {} . Valid until {}'.format(
            cert_meta.name, result['Cert'].not_valid_after.isoformat()))
        sli('Hash is: {}, algo is {}'.format(cks.hash, result['Algo']))

    cert_meta.save_instance(ci)
    return ci
Example #8
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