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
def consolidate_TLSA(cert_meta): """ Consolidate all TLSA RRs for one cert meta. This means TLSA include files are freshly created. @param cert_meta: Cert meta @type cert_meta: cert.Certificate instance @rtype: None @exceptions: """ prepublished_ci = None deployed_ci = None inst_list = cert_meta.active_instances # returns dict with state as key if not inst_list: return for state, ci in inst_list.items(): if state == CertState('prepublished'): if not prepublished_ci: prepublished_ci = ci else: sln('consolidate_TLSA: More than one instance of {} in state' ' "prepublished"'.format(cert_meta.name)) elif state == 'deployed': if not deployed_ci: deployed_ci = ci else: sln('consolidate_TLSA: More than one instance of {} in state' ' "deployed"'.format(cert_meta.name)) if not deployed_ci: sli('consolidate_TLSA: No instance of {} in state "deployed"'.format( cert_meta.name)) return prepublished_TLSA = {} if prepublished_ci: prepublished_TLSA = cert_meta.TLSA_hash(prepublished_ci) deployed_TLSA = cert_meta.TLSA_hash(deployed_ci) distribute_tlsa_rrs( cert_meta, tuple(deployed_TLSA.values()) + tuple(prepublished_TLSA.values()))
def 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
def _get_intermediate_instance(db: db_conn, int_cert: x509.Certificate) -> CertInstance: """ Return CertInstance of intermediate CA cert or create a new CertInstance if not found :param db: Opened DB connection :param int_cert: the CA cert to find the ci for :return: ci of CA cert """ ci = CertKeyStore.ci_from_cert_and_name(db=db, cert=int_cert, name=Misc.SUBJECT_LE_CA) if ci: return ci sln('Storing new intermediate cert.') # intermediate is not in DB - insert it # obtain our cert meta - check, if it exists if Misc.SUBJECT_LE_CA in Certificate.names(db): cm = Certificate.create_or_load_cert_meta( db, Misc.SUBJECT_LE_CA) # yes: we have meta but no instance sln('Cert meta for intermediate cert exists, but no instance.') else: # no: this ist 1st cert with this CA sln('Cert meta for intermediate does not exist, creating {}.'.format( Misc.SUBJECT_LE_CA)) cm = create_CAcert_meta(db=db, name=Misc.SUBJECT_LE_CA, cert_type=CertType('LE')) ci = cm.create_instance(state=CertState('issued'), not_before=int_cert.not_valid_before, not_after=int_cert.not_valid_after) cm.save_instance(ci) ci.store_cert_key(algo=EncAlgoCKS('rsa'), cert=int_cert, key=b'') ##FIXME## might be ec in the future cm.save_instance(ci) return ci
def create_local_ca_cert( db: db_conn, cm: Certificate ) -> Tuple[x509.Certificate, rsa.RSAPrivateKeyWithSerialization, CertInstance]: """ Create a new local CA cert (use an existing one, if one in db) Make it available in globals local_cacert, local_cakey and local_cacert_instance :param db: Opened DB connection :param cm: CA cert Certifcate meta instance :return: Tuple of cacert, cakey and cacert instance and globals local_cacert, local_cakey, local_cacert_instance setup """ global local_cacert, local_cakey, local_cacert_instance # Read pass phrase from user match = False while not match: sli('Please enter passphrase for new CA cert (ASCII only).') try: pp1 = getpass.getpass(prompt='passphrase: ') except UnicodeDecodeError: sle('None-ASCII character found.') continue if pp1 == '': sln('Passphrases must not be empty.') continue sli('Please enter it again.') try: pp2 = getpass.getpass(prompt='passphrase: ') except UnicodeDecodeError: pass if pp1 == pp2: match = True else: sln('Both passphrases differ.') # Generate our key cakey = rsa.generate_private_key(public_exponent=65537, key_size=Misc.LOCAL_CA_BITS, backend=default_backend()) # compose cert # Various details about who we are. For a self-signed certificate the # subject and issuer are always the same. serial = randbits(32) name_dict = X509atts.names subject = issuer = x509.Name([ x509.NameAttribute(x509.NameOID.COUNTRY_NAME, name_dict['C']), x509.NameAttribute(x509.NameOID.LOCALITY_NAME, name_dict['L']), x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, name_dict['O']), x509.NameAttribute(x509.NameOID.COMMON_NAME, name_dict['CN']), ]) not_after = datetime.datetime.utcnow() + datetime.timedelta( days=Misc.LOCAL_CA_LIFETIME) not_before = datetime.datetime.utcnow() - datetime.timedelta(days=1) ci = cm.create_instance(not_before=not_before, not_after=not_after, state=CertState('issued')) ski = x509.SubjectKeyIdentifier.from_public_key(cakey.public_key()) cacert = x509.CertificateBuilder().subject_name(subject).issuer_name( issuer).public_key(cakey.public_key()).serial_number( serial).not_valid_before(not_before).not_valid_after( # Our certificate will be valid for 10 days not_after).add_extension( # CA and no intermediate CAs x509.BasicConstraints(ca=True, path_length=0), critical=True).add_extension( ski, critical=False, # Sign our certificate with our private key ).add_extension(x509.KeyUsage(digital_signature=True, key_cert_sign=True, crl_sign=True, key_encipherment=False, content_commitment=False, data_encipherment=False, key_agreement=False, encipher_only=False, decipher_only=False), critical=True).sign( cakey, hashes.SHA512(), default_backend()) sli('CA cert serial {} with {} bit key, valid until {} created.'.format( serial, Misc.LOCAL_CA_BITS, not_after.isoformat())) ci.store_cert_key(algo='rsa', cert=cacert, key=cakey) cm.save_instance(ci) # CA cert loaded, cache it and return cache_cacert(cacert, cakey, ci) return (cacert, cakey, ci)
def 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
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
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