def _push_live_certificate(self, cert_id, key_type_id): """Moves a new certificate to the live path after checking that everything looks sane""" logger.info("Pushing the new certificate for %s / %s", cert_id, key_type_id) try: private_key = PrivateKeyLoader.load( self._get_path(cert_id, key_type_id, public=False, kind='new')) cert = Certificate.load( self._get_path(cert_id, key_type_id, public=True, kind='new', cert_type='full_chain')) private_key.save( self._get_path(cert_id, key_type_id, public=False, kind='live')) for cert_type, cert_type_details in CERTIFICATE_TYPES.items(): cert.save(self._get_path(cert_id, key_type_id, public=True, kind='live', cert_type=cert_type), mode=cert_type_details['save_mode']) except (OSError, X509Error): logger.exception("Problem pushing live certificate %s / %s", cert_id, key_type_id) return CertificateStatus.CERTIFICATE_ISSUED return CertificateStatus.VALID
def create_initial_certs(self): """ Creates initial certificates for everything that doesn't currently exist. This is so that web servers which depend on having a certificate to start can start and begin serving traffic so they can forward ACME challenges through to us - that will enable us to request a real certificate to replace our initial one. """ for cert_id in self.cert_status: for key_type_id, key_type_details in KEY_TYPES.items(): if self.cert_status[cert_id][ key_type_id].status != CertificateStatus.INITIAL: continue logger.info( "Creating initial self-signed certificate for %s / %s", cert_id, key_type_id) key = key_type_details['class']() key.generate(**key_type_details['params']) key.save( self._get_path(cert_id, key_type_id, public=False, kind='live')) cert = Certificate( SelfSignedCertificate( private_key=key, common_name="Snakeoil cert", sans=(), from_date=datetime.datetime.utcnow(), until_date=datetime.datetime.utcnow() + datetime.timedelta(days=3), ).pem) for cert_type, cert_type_details in CERTIFICATE_TYPES.items(): path = self._get_path(cert_id, key_type_id, public=True, kind='live', cert_type=cert_type) cert.save(path, mode=cert_type_details['save_mode']) self.cert_status[cert_id][ key_type_id].status = CertificateStatus.SELF_SIGNED
def _get_certificate_status(cert_id, key_type_id, certificate): # pylint: disable=too-many-return-statements try: new_cert_path = self._get_path(cert_id, key_type_id, public=True, kind='new', cert_type='full_chain') new_cert = Certificate.load(new_cert_path) if new_cert.certificate.not_valid_before > certificate.certificate.not_valid_before: return CertificateStatus.READY_TO_BE_PUSHED except OSError: pass if certificate.self_signed is True: return CertificateStatus.SELF_SIGNED if datetime.datetime.utcnow( ) > certificate.certificate.not_valid_after: logger.warning("Certificate %s type %s expired on %s", cert_id, key_type_id, certificate.certificate.not_valid_after) return CertificateStatus.EXPIRED if certificate.needs_renew(): return CertificateStatus.NEEDS_RENEWAL cur_cn = certificate.common_name.lower() new_cn = self.config.certificates[cert_id]['CN'].lower() if cur_cn != new_cn: logger.warning( 'Certificate %s type %s has CN %s but is configured for %s, moving back to re-issue', cert_id, key_type_id, cur_cn, new_cn) return CertificateStatus.SUBJECTS_CHANGED cur_sans = { san.lower() for san in certificate.subject_alternative_names } new_sans = { san.lower() for san in self.config.certificates[cert_id]['SNI'] } if cur_sans != new_sans: logger.warning( 'Certificate %s type %s has SANs %s but is configured for %s, moving back to re-issue', cert_id, key_type_id, cur_sans, new_sans) return CertificateStatus.SUBJECTS_CHANGED return CertificateStatus.VALID
def get_certificate(self, csr_id, deadline=None): """ Returns the certificate and the full chain (if present) wrapped in a x509.Certificate instance. This should be called after the order has been finalized. """ if deadline is None: # using now() instead of utcnow() cause acme_client uses now() # and using utcnow() on systems where now() != utcnow() cause # unexpected behaviour deadline = datetime.now() + timedelta(seconds=10) finished_order = self._get_order(csr_id) try: certificate_order = self.acme_client.fetch_certificate( finished_order, deadline=deadline) except errors.TimeoutError: raise ACMETimeoutFetchingCertificateError( 'Timeout waiting for the ACME directory to finalize the order') except errors.IssuanceError as issuance_error: self._clean(csr_id) raise ACMEError('Unable to get certificate') from issuance_error except requests.exceptions.RequestException as request_error: raise ACMETransportError( 'Unable to fetch certificate') from request_error self._clean(csr_id) try: certificate = Certificate( certificate_order.fullchain_pem.encode('utf-8')) except X509Error as certificate_error: raise ACMEIssuedCertificateError( 'Received invalid PEM from ACME server') from certificate_error return certificate
def _handle_ready_to_be_pushed(self, cert_id, key_type_id): """Handles READY_TO_BE_PUSHED status. Performs the following actions: - Checks if the certificate is ready to be pushed to live_certs_path - Pushes the cerfificate iff it's ready to be pushed. """ try: cert = Certificate.load( self._get_path(cert_id, key_type_id, public=True, kind='new', cert_type='full_chain')) staging_timedelta = self.config.certificates[cert_id][ 'staging_time'] if cert.certificate.not_valid_before >= ( datetime.datetime.utcnow() - staging_timedelta): return CertificateStatus.READY_TO_BE_PUSHED except (OSError, X509Error): logger.exception( "Problem verifying not valid before date on certificate %s / %s", cert_id, key_type_id) return CertificateStatus.CERTIFICATE_ISSUED return self._push_live_certificate(cert_id, key_type_id)
def _set_cert_status(self): """ Figures out the current status for every configured certificate """ state = collections.defaultdict(dict) def _get_certificate_status(cert_id, key_type_id, certificate): # pylint: disable=too-many-return-statements try: new_cert_path = self._get_path(cert_id, key_type_id, public=True, kind='new', cert_type='full_chain') new_cert = Certificate.load(new_cert_path) if new_cert.certificate.not_valid_before > certificate.certificate.not_valid_before: return CertificateStatus.READY_TO_BE_PUSHED except OSError: pass if certificate.self_signed is True: return CertificateStatus.SELF_SIGNED if datetime.datetime.utcnow( ) > certificate.certificate.not_valid_after: logger.warning("Certificate %s type %s expired on %s", cert_id, key_type_id, certificate.certificate.not_valid_after) return CertificateStatus.EXPIRED if certificate.needs_renew(): return CertificateStatus.NEEDS_RENEWAL cur_cn = certificate.common_name.lower() new_cn = self.config.certificates[cert_id]['CN'].lower() if cur_cn != new_cn: logger.warning( 'Certificate %s type %s has CN %s but is configured for %s, moving back to re-issue', cert_id, key_type_id, cur_cn, new_cn) return CertificateStatus.SUBJECTS_CHANGED cur_sans = { san.lower() for san in certificate.subject_alternative_names } new_sans = { san.lower() for san in self.config.certificates[cert_id]['SNI'] } if cur_sans != new_sans: logger.warning( 'Certificate %s type %s has SANs %s but is configured for %s, moving back to re-issue', cert_id, key_type_id, cur_sans, new_sans) return CertificateStatus.SUBJECTS_CHANGED return CertificateStatus.VALID for cert_id in self.config.certificates: for key_type_id in KEY_TYPES: try: current_status = self.cert_status[cert_id][key_type_id] if current_status in ( CertificateStatus.CSR_PUSHED, CertificateStatus.CHALLENGES_PUSHED, CertificateStatus.CHALLENGES_REJECTED, CertificateStatus.CERTIFICATE_ISSUED, CertificateStatus.ACMECHIEF_ERROR, CertificateStatus.ACMEDIR_ERROR): # we don't want to break the current cert. issue process continue except KeyError: pass try: certificate = Certificate.load( self._get_path(cert_id, key_type_id, public=True, kind='live')) new_status = _get_certificate_status( cert_id, key_type_id, certificate) except (OSError, X509Error): new_status = CertificateStatus.INITIAL state[cert_id][key_type_id] = CertificateState(new_status) return state
def test_certificate(self): from_date = datetime.datetime.utcnow() until_date = from_date + datetime.timedelta(days=90) initial_cert = get_self_signed_certificate(from_date, until_date) cert = Certificate(initial_cert.pem) self.assertIsInstance(cert.certificate, crypto_x509.Certificate) self.assertEqual(cert.chain, [cert]) self.assertFalse(cert.needs_renew()) self.assertTrue(cert.self_signed) mocked_now = until_date - datetime.timedelta(days=10) with mock.patch('acme_chief.x509.datetime') as mocked_datetime: mocked_datetime.utcnow = mock.Mock(return_value=mocked_now) self.assertTrue(cert.needs_renew()) with tempfile.TemporaryDirectory() as temp_dir: full_chain_cert = Certificate(FULL_CHAIN) # sanity check self.assertEqual(len(full_chain_cert.chain), 2) self.assertEqual( full_chain_cert.chain[0].certificate.serial_number, FIRST_CERT_SERIAL_NUMBER) self.assertEqual( full_chain_cert.chain[1].certificate.serial_number, SECOND_CERT_SERIAL_NUMBER) cert_only_path = os.path.join(temp_dir, 'cert.crt') chain_only_path = os.path.join(temp_dir, 'chain.crt') full_chain_path = os.path.join(temp_dir, 'chained.crt') full_chain_cert.save(cert_only_path, mode=CertificateSaveMode.CERT_ONLY) full_chain_cert.save(chain_only_path, mode=CertificateSaveMode.CHAIN_ONLY) full_chain_cert.save(full_chain_path, mode=CertificateSaveMode.FULL_CHAIN) cert_only = Certificate.load(cert_only_path) self.assertEqual(len(cert_only.chain), 1) self.assertEqual(cert_only.certificate.serial_number, FIRST_CERT_SERIAL_NUMBER) chain_only = Certificate.load(chain_only_path) self.assertEqual(len(chain_only.chain), 1) self.assertEqual(chain_only.certificate.serial_number, SECOND_CERT_SERIAL_NUMBER) full_chain = Certificate.load(full_chain_path) self.assertEqual(len(full_chain.chain), 2) self.assertEqual(full_chain.chain[0].certificate.serial_number, FIRST_CERT_SERIAL_NUMBER) self.assertEqual(full_chain.chain[1].certificate.serial_number, SECOND_CERT_SERIAL_NUMBER)