Beispiel #1
0
def export_instance(db: db_conn) -> bool:
    """
    Export certs and keys of one CertInstance
    :param db: Opened database handle
    :return: True on success
    """
    opts = get_options()

    name = Certificate.fqdn_from_instance_serial(db, opts.cert_serial)
    cert_meta = Certificate.create_or_load_cert_meta(db, name)
    for ci in cert_meta.cert_instances:
        if ci.row_id == opts.cert_serial:
            for cks in ci.cksd.values():
                algo = cks.algo
                cert = cks.cert
                key = cks.key

                cert_path = Path(Pathes.work) / 'cert-{}-{}.pem'.format(
                    opts.cert_serial, algo)
                with open(str(cert_path), 'w') as fde:
                    fde.write(cert)

                key_path = Path(Pathes.work) / 'key-{}-{}.pem'.format(
                    opts.cert_serial, algo)
                with open(str(key_path), 'w') as fde:
                    fde.write(key)
                    key_path.chmod(0o400)

                sli('Cert and {} key for {} exported to {} and {}'.format(
                    algo, cert_meta.name, str(cert_path), str(key_path)))
    return True
Beispiel #2
0
def delete_TLSA(cert_meta: Certificate) -> None:
    """
    Delete all TLSA RRs per fqdn of all altnames either in flatfile (make include file empty) or in dyn dns
    :param cert_meta:
    :return:
    """

    if Pathes.tlsa_dns_master == '':  # DNS master on local host

        if Misc.LE_ZONE_UPDATE_METHOD == 'zone_file':

            for (zone, fqdn) in cert_meta.zone_and_FQDN_from_altnames():
                filename = fqdn + '.tlsa'
                dest = str(Pathes.zone_file_root / zone / filename)

                #just open for write without writing, which makes file empty
                with open(dest, 'w') as fd:
                    sli('Truncating {}'.format(dest))
                updateZoneCache(zone)

        elif Misc.LE_ZONE_UPDATE_METHOD == 'ddns':

            zones = {}
            for (zone, fqdn) in cert_meta.zone_and_FQDN_from_altnames():
                if zone in zones:
                    if fqdn not in zones[zone]: zones[zone].append(fqdn)
                else:
                    zones[zone] = [fqdn]
            for zone in zones:
                the_update = ddns_update(zone)
                for fqdn in zones[zone]:
                    for prefix in cert_meta.tlsaprefixes.keys():
                        tag = str(prefix.format(fqdn)).split(maxsplit=1)[0]
                        sld('Deleting TLSA with tag {} an fqdn {} in zone {}'.
                            format(tag, fqdn, zone))
                        the_update.delete(tag)
                response = dns_query.tcp(the_update, '127.0.0.1', timeout=10)
                rc = response.rcode()
                if rc != 0:
                    sle('DNS update failed for zone {} with rcode: {}:\n{}'.
                        format(zone, response.rcode.to_text(rc),
                               response.rcode))
                    raise Exception(
                        'DNS update failed for zone {} with rcode: {}'.format(
                            zone, response.rcode.to_text(rc)))
Beispiel #3
0
 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)
Beispiel #4
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
Beispiel #5
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
Beispiel #6
0
def create_CAcert_meta(
        db: db_conn,
        name: str,
        cert_type: Optional[CertType] = None) -> Optional[Certificate]:
    """
    Lookup or create a CA cert meta in rows ceetificates and subjects
    :param db:          opened database connection in read/write transaction
    :param name:        name of CA cert (as configured in config: Misc.SUBJECT_LOCAL_CA or SUBJECT_LE_CA)
    :cert_type:         CertType
    :return:            cert meta or None
    """
    if name not in (Misc.SUBJECT_LOCAL_CA, Misc.SUBJECT_LE_CA):
        raise AssertionError(
            'create_CAcert_meta: argument name "{} invalid"'.format(name))
    cm = Certificate.ca_cert_meta(db, name, cert_type)
    if not cm:
        sle('Failed to create CA cert meta for {}'.format(name))
        sys.exit(1)
    return cm
Beispiel #7
0
def issue_local_CAcert(db: db_conn) -> bool:
    """
    Issue a local CA cert and store it in DB.
    :param db: Opened DB connection
    :return: True, if CA cert created and stored in DB, False otherwise
    """

    sli('Creating local CA certificate.')
    cm = Certificate.create_or_load_cert_meta(db, name=Misc.SUBJECT_LOCAL_CA)
    if not cm.in_db:  # loaded from DB?
        # 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:
        create_local_ca_cert(db, cm)
    except Exception as e:
        sle('Failed to create local CA cert, because: {}'.format(str(e)))
        return False
    return True
Beispiel #8
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)
Beispiel #9
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)
Beispiel #10
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
Beispiel #11
0
def execute_from_command_line():
    """
    Called from comand line
    :return: sys.exit(0) on success, False or sys.exit(1) otherwise
    """

    try:  # try to contact IDE (PyCharm)
        from serverPKI.settrace import connect_to_ide
        connect_to_ide()
    except Exception:  # no IDE configured
        pass

    all_cert_names: List[str, ...] = []
    our_cert_names: List[str, ...] = []
    our_certs: Dict[str, Certificate] = {}

    ##util.log_to_file('sftp.log')

    opts = parse_options()  # globals not re-initialized
    parse_config()  # globals not re-initialized
    (DBAccount, Misc, Pathes, X509atts) = get_config()
    db_name = DBAccount.dbDatabase

    sli('operateCA [{}-{}] started with options {}'.format(
        db_name, get_version_string(), options_set()))
    check_actions()

    pe = dbc('serverpki')
    db: db_conn = pe.open()

    # in case of restarting serverPKI wo fresh imports (e.g. by pytest)
    init_module_cert()

    LocalCaCertCache.cert = None
    LocalCaCertCache.key = None
    LocalCaCertCache.ci = None

    read_db_encryption_key(db)

    all_cert_names = Certificate.names(db)
    all_disthost_names = Certificate.disthost_names(db)
    # preload CMs, CIs and CKSs of our CAs
    cas = []
    for name in (Misc.SUBJECT_LOCAL_CA, Misc.SUBJECT_LE_CA):
        if name in all_cert_names:  # does CA exist in DB?
            cm = Certificate.create_or_load_cert_meta(db, name)
            cas.append(name)

    sli('{} certificates and CAs {} in DB'.format(len(all_cert_names), cas))

    if opts.encrypt:
        if encrypt_all_keys(db):
            sys.exit(0)
        sys.exit(1)
    if opts.decrypt:
        if decrypt_all_keys(db):
            sys.exit(0)
        sys.exit(1)
    if opts.issue_local_cacert:
        if issue_local_CAcert(db):
            sys.exit(0)
        sys.exit(1)

    if opts.all:
        our_cert_names = all_cert_names
    elif opts.remaining_days:
        our_cert_names = names_of_local_certs_to_be_renewed(
            db, opts.remaining_days, opts.distribute)
        if not opts.create:
            opts.create = not opts.distribute

    cert_name_set: set = set(our_cert_names)

    error = False

    if opts.only_cert:
        error = False
        for i in opts.only_cert:
            if i not in all_cert_names:
                sle("{} not in configuration. Can't be specified with --only".
                    format(i))
                error = True
        if not error:
            cert_name_set = set(opts.only_cert)

    else:
        if opts.cert_to_be_included:
            error = False
            for i in opts.cert_to_be_included:
                if i not in all_cert_names:
                    sle("{} not in configuration. Can't be included".format(i))
                    error = True
            if not error:
                cert_name_set = set(opts.cert_to_be_included)

        if opts.cert_to_be_excluded:
            error = False
            for i in opts.cert_to_be_excluded:
                if i not in all_cert_names:
                    sle("{} not in configuration. Can't be excluded".format(i))
                    error = True
            if not error:
                cert_name_set -= set(opts.cert_to_be_excluded)
    if opts.only_host:
        for i in opts.only_host:
            if i not in all_disthost_names:
                sle("{} not in configuration. Can't use in --limit-to-disthost"
                    .format(i))
                error = True
    if opts.skip_host:
        for i in opts.skip_host:
            if i not in all_disthost_names:
                sle("{} not in configuration. Can't use in --skip-disthost".
                    format(i))
                error = True

    if error:
        sle('Stopped due to command line errors')
        sys.exit(1)

    our_cert_names = sorted(list(cert_name_set))

    for name in our_cert_names:
        cm = Certificate.create_or_load_cert_meta(db, name)
        if cm.in_db: our_certs[name] = cm

    if opts.check_only and not opts.schedule:
        sli('No syntax errors found in configuration.')
        ##sli('Selected certificates:\n\r{}'.format(our_cert_names))
        print_certs(db, our_cert_names)
        sys.exit(0)

    sld('Selected certificates:\n\r{}'.format(our_cert_names))

    if opts.schedule:
        sli('Scheduling actions.')
        scheduleCerts(db, our_certs)
    else:
        if opts.create:
            sli('Creating certificates.')
            for c in our_certs.values():
                if c.cert_type == CertType('LE'):
                    if issue_LE_cert(c):
                        continue
                elif c.cert_type == CertType('local'):
                    if issue_local_cert(c):
                        continue
                else:
                    raise AssertionError('Invalid CertType in {}'.format(
                        c.name))
                sle('Stopped due to error')
                sys.exit(1)

        if opts.distribute:
            sli('Distributing certificates.')
            deployCerts(our_certs)

    if opts.sync_disk:
        for c in our_certs.values():
            consolidate_cert(c)

    if opts.sync_tlsas:
        for c in our_certs.values():
            consolidate_TLSA(c)
        updateSOAofUpdatedZones()

    if opts.remove_tlsas:
        for c in our_certs.values():
            delete_TLSA(c)
        updateSOAofUpdatedZones()

    if opts.cert_serial:
        sli('Exporting certificate instance.')
        export_instance(db)

    if opts.register:
        sli('Registering a new Let\'s Encrypt Account.\n With URI:{}\n'
            ' and e-mail {}'.format(Misc.LE_SERVER, Misc.LE_EMAIL))
        register(Misc.LE_SERVER, Pathes.le_account, Misc.LE_EMAIL, None)
Beispiel #12
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
Beispiel #13
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
Beispiel #14
0
def distribute_tlsa_rrs(cert_meta: Certificate,
                        hashes: Union[Tuple[str], List[str]]) -> None:
    """
    Distribute TLSA RR.
    Puts TLSA RR fqdn into DNS zone, by dynamic dns or editing zone file and updating zone cache.
    If cert has altnames, one set of TLSA RRs is inserted per altname and per TLSA prefix.
    :param cert_meta:
    :param hashes: list of hashes, may include active and prepublishes hashes for all algos
    :return:
    """

    if len(cert_meta.tlsaprefixes) == 0: return

    sli('Distributing TLSA RRs for DANE.')

    if Pathes.tlsa_dns_master == '':  # DNS master on local host

        if Misc.LE_ZONE_UPDATE_METHOD == 'zone_file':

            for (zone, fqdn) in zone_and_FQDN_from_altnames(cert_meta):
                filename = fqdn + '.tlsa'
                dest = str(Pathes.zone_file_root / zone / filename)
                sli('{} => {}'.format(filename, dest))
                tlsa_lines = []
                for prefix in cert_meta.tlsaprefixes.keys():
                    for hash in hashes:
                        tlsa_lines.append(
                            str(prefix.format(fqdn) + ' ' + hash + '\n'))
                with open(dest, 'w') as fd:
                    fd.writelines(tlsa_lines)
                updateZoneCache(zone)

        elif Misc.LE_ZONE_UPDATE_METHOD == 'ddns':

            tlsa_datatype = rdatatype.from_text('TLSA')
            zones = {}
            for (zone, fqdn) in cert_meta.zone_and_FQDN_from_altnames():
                if zone in zones:
                    if fqdn not in zones[zone]: zones[zone].append(fqdn)
                else:
                    zones[zone] = [fqdn]
            for zone in zones:
                the_update = ddns_update(zone)
                for fqdn in zones[zone]:
                    for prefix in cert_meta.tlsaprefixes.keys():
                        pf_with_fqdn = str(prefix.format(fqdn))
                        fields = pf_with_fqdn.split(maxsplit=4)
                        sld('Deleting possible old TLSAs: {}'.format(
                            fields[0]))
                        the_update.delete(fields[0], tlsa_datatype)

                        for hash in hashes:
                            sld('Adding TLSA: {} {} {} {}'.format(
                                fields[0], int(fields[1]), fields[3],
                                fields[4] + ' ' + hash))
                            the_update.add(fields[0], int(fields[1]),
                                           fields[3], fields[4] + ' ' + hash)

                response = dns_query.tcp(the_update, '127.0.0.1', timeout=10)
                rc = response.rcode()
                if rc != 0:
                    sle('DNS update failed for zone {} with rcode: {}:\n{}'.
                        format(zone, response.rcode.to_text(rc),
                               response.rcode))
                    raise Exception(
                        'DNS update add failed for zone {} with rcode: {}'.
                        format(zone, response.rcode.to_text(rc)))

    else:  # remote DNS master ( **INCOMPLETE**)
        sle('Remote DNS master server is currently not supported. Must be on same host as this script.'
            )
        exit(1)
        with ssh_connection(Pathes.tlsa_dns_master) as client:
            with client.open_sftp() as sftp:
                chdir(str(Pathes.work_tlsa))
                p = Path('.')
                sftp.chdir(str(Pathes.zone_file_root))

                for child_dir in p.iterdir():
                    for child in child_dir.iterdir():
                        sli('{} => {}:{}'.format(child, Pathes.tlsa_dns_master,
                                                 child))
                        fat = sftp.put(str(child), str(child), confirm=True)
                        sld('size={}, uid={}, gid={}, mtime={}'.format(
                            fat.st_size, fat.st_uid, fat.st_gid, fat.st_mtime))