def rollback_checkpoints(self, rollback=1): """Revert 'rollback' number of configuration checkpoints. :param rollback: Number of checkpoints to reverse :type rollback: int """ try: rollback = int(rollback) except: logger.error("Rollback argument must be a positive integer") # Sanity check input if rollback < 1: logger.error("Rollback argument must be a positive integer") return backups = os.listdir(CONFIG.BACKUP_DIR) backups.sort() if len(backups) < rollback: logger.error(("Unable to rollback %d checkpoints, only " "%d exist") % (rollback, len(backups))) while rollback > 0 and backups: cp_dir = CONFIG.BACKUP_DIR + backups.pop() result = self._recover_checkpoint(cp_dir) if result != 0: logger.fatal("Failed to load checkpoint during rollback") sys.exit(39) rollback -= 1 self.aug.load()
def get_cas(self): DV_choices = [] OV_choices = [] EV_choices = [] choices = [] try: with open("/etc/letsencrypt/.ca_offerings") as f: for line in f: choice = line.split(";", 1) if 'DV' in choice[0]: DV_choices.append(choice) elif 'OV' in choice[0]: OV_choices.append(choice) else: EV_choices.append(choice) # random.shuffle(DV_choices) # random.shuffle(OV_choices) # random.shuffle(EV_choices) choices = DV_choices + OV_choices + EV_choices choices = [(l[0], l[1]) for l in choices] except IOError as e: logger.fatal("Unable to find .ca_offerings file") sys.exit(1) return choices
def __init__(self, ca_server, cert_signing_request=None, private_key=None, use_curses=True): global dialog self.curses = use_curses # Logger needs to be initialized before Configurator self.init_logger() # TODO: Can probably figure out which configurator to use without # special packaging based on system info # Command line arg or client function to discover self.config = apache_configurator.ApacheConfigurator(CONFIG.SERVER_ROOT) self.server = ca_server self.csr_file = cert_signing_request self.key_file = private_key # If CSR is provided, the private key should also be provided. # TODO: Make sure key was actually used in CSR # TODO: Make sure key has proper permissions if self.csr_file and not self.key_file: logger.fatal("Please provide the private key file used in \ generating the provided CSR") sys.exit(1) self.server_url = "https://%s/acme/" % self.server
def rollback_checkpoints(self, rollback=1): """Revert 'rollback' number of configuration checkpoints.""" try: rollback = int(rollback) except: logger.error("Rollback argument must be a positive integer") # Sanity check input if rollback < 1: logger.error("Rollback argument must be a positive integer") return backups = os.listdir(CONFIG.BACKUP_DIR) backups.sort() if len(backups) < rollback: logger.error(("Unable to rollback %d checkpoints, only " "%d exist") % (rollback, len(backups))) while rollback > 0 and backups: cp_dir = CONFIG.BACKUP_DIR + backups.pop() result = self.__recover_checkpoint(cp_dir) if result != 0: logger.fatal("Failed to load checkpoint during rollback") sys.exit(39) rollback -= 1 self.aug.load()
def acme_authorization(self, challenge_msg, chal_objs, responses): """Handle ACME "authorization" phase. :param challenge_msg: ACME "challenge" message. :type challenge_msg: dict :param chal_objs: TODO :type chal_objs: TODO :param responses: TODO :type responses: TODO :returns: ACME "authorization" message. :rtype: dict """ auth_dict = self.send( acme.authorization_request(challenge_msg["sessionID"], self.names[0], challenge_msg["nonce"], responses, self.key_file)) try: return self.is_expected_msg(auth_dict, "authorization") except: logger.fatal("Failed Authorization procedure - " "cleaning up challenges") sys.exit(1) finally: self.cleanup_challenges(chal_objs)
def handle_challenge(self): challenge_dict = self.send(self.challenge_request(self.names)) try: return self.is_expected_msg(challenge_dict, "challenge") except: logger.fatal("Unexpected error") sys.exit(1)
def verify_identity(self, c): path = self.gen_challenge_path( c["challenges"], c.get("combinations", None)) logger.info("Performing the following challenges:") # Every indicies element is a list of integers referring to which # challenges in the master list the challenge object satisfies # Single Challenge objects that can satisfy multiple server challenges # mess up the order of the challenges, thus requiring the indicies challenge_objs, indicies = self.challenge_factory( self.names[0], c["challenges"], path) responses = [None] * len(c["challenges"]) # Perform challenges and populate responses for i, c_obj in enumerate(challenge_objs): if not c_obj.perform(): logger.fatal("Challenge Failed") sys.exit(1) for index in indicies[i]: responses[index] = c_obj.generate_response() logger.info("Configured Apache for challenges; " + "waiting for verification...") return responses, challenge_objs
def __find_smart_path(self, challenges, combos): """ Can be called if combinations is included Function uses a simple ranking system to choose the combo with the lowest cost """ chall_cost = {} max_cost = 0 for i, chall in enumerate(CHALLENGE_PREFERENCES): chall_cost[chall] = i max_cost += i best_combo = [] # Set above completing all of the available challenges best_combo_cost = max_cost + 1 combo_total = 0 for combo in combos: for c in combo: combo_total += chall_cost.get(challenges[c]["type"], max_cost) if combo_total < best_combo_total: best_combo = combo combo_total = 0 if not best_combo: logger.fatal("Client does not support any combination of \ challenges to satisfy ACME server") sys.exit(22) return best_combo
def challenge_factory(self, name, challenges, path): sni_todo = [] # Since a single invocation of SNI challenge can satsify multiple # challenges. We must keep track of all the challenges it satisfies sni_satisfies = [] challenge_objs = [] challenge_obj_indicies = [] for c in path: if challenges[c]["type"] == "dvsni": logger.info("\tDVSNI challenge for name %s." % name) sni_satisfies.append(c) sni_todo.append((str(name), str(challenges[c]["r"]), str(challenges[c]["nonce"]))) elif challenges[c]["type"] == "recoveryToken": logger.info("\tRecovery Token Challenge for name: %s." % name) challenge_objs_indicies.append(c) challenge_objs.append(RecoveryToken()) else: logger.fatal("Challenge not currently supported") sys.exit(82) if sni_todo: # SNI_Challenge can satisfy many sni challenges at once so only # one "challenge object" is issued for all sni_challenges challenge_objs.append( SNI_Challenge(sni_todo, os.path.abspath(self.key_file), self.config)) challenge_obj_indicies.append(sni_satisfies) logger.debug(sni_todo) return challenge_objs, challenge_obj_indicies
def __init__(self, ca_server, cert_signing_request=None, private_key=None, use_curses=True): self.curses = use_curses # Logger needs to be initialized before Configurator self.init_logger() # TODO: Can probably figure out which configurator to use # without special packaging based on system info Command # line arg or client function to discover self.config = apache_configurator.ApacheConfigurator( CONFIG.SERVER_ROOT) self.server = ca_server if cert_signing_request: self.csr_file = cert_signing_request.name else: self.csr_file = None if private_key: self.key_file = private_key.name else: self.key_file = None # TODO: Figure out all exceptions from this function try: self._validate_csr_key_cli() except Exception as e: # TODO: Something nice here... logger.fatal(("%s - until the programmers get their act together, " "we are just going to exit" % str(e))) sys.exit(1) self.server_url = "https://%s/acme/" % self.server
def __init__(self, ca_server, cert_signing_request=None, private_key=None, use_curses=True): global dialog self.curses = use_curses # Logger needs to be initialized before Configurator self.init_logger() self.config = configurator.Configurator(SERVER_ROOT) self.server = ca_server self.csr_file = cert_signing_request self.key_file = private_key # If CSR is provided, the private key should also be provided. # TODO: Make sure key was actually used in CSR # TODO: Make sure key has proper permissions if self.csr_file and not self.key_file: logger.fatal("Please provide the private key file used in \ generating the provided CSR") sys.exit(1) self.server_url = "https://%s/acme/" % self.server
def verify_identity(self, c): path = self.gen_challenge_path(c["challenges"], c.get("combinations", None)) logger.info("Peforming the following challenges:") # Every indicies element is a list of integers referring to which # challenges in the master list the challenge object satisfies # Single Challenge objects that can satisfy multiple server challenges # mess up the order of the challenges, thus requiring the indicies challenge_objs, indicies = self.challenge_factory( self.names[0], c["challenges"], path) responses = [None] * len(c["challenges"]) # Perform challenges and populate responses for i, c_obj in enumerate(challenge_objs): if not c_obj.perform(): logger.fatal("Challenge Failed") sys.exit(1) for index in indicies[i]: responses[index] = c_obj.generate_response() logger.info("Configured Apache for challenges; \ waiting for verification...") return responses, challenge_objs
def _remove_contained_files(self, file_list): """Erase all files contained within file_list. :param file_list: file containing list of file paths to be deleted :type file_list: str :returns: Success :rtype: bool """ # Check to see that file exists to differentiate can't find file_list # and can't remove filepaths within file_list errors. if not os.path.isfile(file_list): return False try: with open(file_list, 'r') as f: filepaths = f.read().splitlines() for fp in filepaths: # Files are registered before they are added... so # check to see if file exists first if os.path.lexists(fp): os.remove(fp) else: logger.warn(( "File: %s - Could not be found to be deleted\n" "Program was probably shut down unexpectedly, " "in which case this is not a problem") % fp) except IOError: logger.fatal( "Unable to remove filepaths contained within %s" % file_list) sys.exit(41) return True
def challenge_factory(self, name, challenges, path): sni_todo = [] # Since a single invocation of SNI challenge can satisfy multiple # challenges. We must keep track of all the challenges it satisfies sni_satisfies = [] challenge_objs = [] challenge_obj_indicies = [] for c in path: if challenges[c]["type"] == "dvsni": logger.info(" DVSNI challenge for name %s." % name) sni_satisfies.append(c) sni_todo.append( (str(name), str(challenges[c]["r"]), str(challenges[c]["nonce"])) ) elif challenges[c]["type"] == "recoveryToken": logger.info("\tRecovery Token Challenge for name: %s." % name) challenge_objs_indicies.append(c) challenge_objs.append(RecoveryToken()) else: logger.fatal("Challenge not currently supported") sys.exit(82) if sni_todo: # SNI_Challenge can satisfy many sni challenges at once so only # one "challenge object" is issued for all sni_challenges challenge_objs.append(SNI_Challenge( sni_todo, os.path.abspath(self.key_file), self.config)) challenge_obj_indicies.append(sni_satisfies) logger.debug(sni_todo) return challenge_objs, challenge_obj_indicies
def __remove_contained_files(self, file_list): """ Erase any files contained within the text file, file_list """ # Check to see that file exists to differentiate can't find file_list # and can't remove filepaths within file_list errors. if not os.path.isfile(file_list): return False try: with open(file_list, 'r') as f: filepaths = f.read().splitlines() for fp in filepaths: # Files are registered before they are added... so # check to see if file exists first if os.path.lexists(fp): os.remove(fp) else: logger.warn(( "File: %s - Could not be found to be deleted\n" "Program was probably shut down unexpectedly, " "in which case this is not a problem") % fp) except IOError: logger.fatal( "Unable to remove filepaths contained within %s" % file_list) sys.exit(41) return True
def acme_authorization(self, challenge_msg, chal_objs, responses): """Handle ACME "authorization" phase. :param challenge_msg: ACME "challenge" message. :type challenge_msg: dict :param chal_objs: TODO :type chal_objs: TODO :param responses: TODO :type responses: TODO :returns: ACME "authorization" message. :rtype: dict """ auth_dict = self.send(acme.authorization_request( challenge_msg["sessionID"], self.names[0], challenge_msg["nonce"], responses, self.key_file)) try: return self.is_expected_msg(auth_dict, "authorization") except: logger.fatal("Failed Authorization procedure - " "cleaning up challenges") sys.exit(1) finally: self.cleanup_challenges(chal_objs)
def handle_certificate(self, csr_der): certificate_dict = self.send( self.certificate_request(csr_der, self.key_file)) try: return self.is_expected_msg(certificate_dict, "certificate") except: logger.fatal("Encountered unexpected message") sys.exit(1)
def challenge_factory(self, name, challenges, path): """ :param name: TODO :type name: TODO :param challanges: A list of challenges from ACME "challenge" server message to be fulfilled by the client in order to prove possession of the identifier. :type challenges: list :param path: List of indices from `challenges`. :type path: list :returns: A pair of TODO :rtype: tuple """ sni_todo = [] # Since a single invocation of SNI challenge can satisfy multiple # challenges. We must keep track of all the challenges it satisfies sni_satisfies = [] challenge_objs = [] challenge_obj_indices = [] for index in path: chall = challenges[index] if chall["type"] == "dvsni": logger.info(" DVSNI challenge for name %s." % name) sni_satisfies.append(index) sni_todo.append( (str(name), str(chall["r"]), str(chall["nonce"]))) elif chall["type"] == "recoveryToken": logger.info("\tRecovery Token Challenge for name: %s." % name) challenge_obj_indices.append(index) challenge_objs.append({ type: "recoveryToken", }) else: logger.fatal("Challenge not currently supported") sys.exit(82) if sni_todo: # SNI_Challenge can satisfy many sni challenges at once so only # one "challenge object" is issued for all sni_challenges challenge_objs.append({ "type": "dvsni", "listSNITuple": sni_todo, "dvsni_key": os.path.abspath(self.key_file), }) challenge_obj_indices.append(sni_satisfies) logger.debug(sni_todo) return challenge_objs, challenge_obj_indices
def get_key_csr_pem(self, csr_return_format='der'): """Return key and CSR, generate if necessary. Returns key and CSR using provided files or generating new files if necessary. Both will be saved in PEM format on the filesystem. The CSR can optionally be returned in DER format as the CSR cannot be loaded back into M2Crypto. :param csr_return_format: If "der" returned CSR is in DER format, PEM otherwise. :param csr_return_format: str :returns: A pair of `(key, csr)`, where `key` is PEM encoded `str` and `csr` is PEM/DER (depedning on `csr_return_format` encoded `str`. :rtype: tuple """ key_pem = None csr_pem = None if not self.key_file: key_pem = crypto_util.make_key(CONFIG.RSA_KEY_SIZE) # Save file le_util.make_or_verify_dir(CONFIG.KEY_DIR, 0o700) key_f, self.key_file = le_util.unique_file( os.path.join(CONFIG.KEY_DIR, "key-letsencrypt.pem"), 0o600) key_f.write(key_pem) key_f.close() logger.info("Generating key: %s" % self.key_file) else: try: key_pem = open(self.key_file).read().replace("\r", "") except: logger.fatal("Unable to open key file: %s" % self.key_file) sys.exit(1) if not self.csr_file: csr_pem, csr_der = crypto_util.make_csr(self.key_file, self.names) # Save CSR le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0o755) csr_f, self.csr_file = le_util.unique_file( os.path.join(CONFIG.CERT_DIR, "csr-letsencrypt.pem"), 0o644) csr_f.write(csr_pem) csr_f.close() logger.info("Creating CSR: %s" % self.csr_file) else: try: csr = M2Crypto.X509.load_request(self.csr_file) csr_pem, csr_der = csr.as_pem(), csr.as_der() except: logger.fatal("Unable to open CSR file: %s" % self.csr_file) sys.exit(1) if csr_return_format == 'der': return key_pem, csr_der else: return key_pem, csr_pem
def challenge_factory(self, name, challenges, path): """ :param name: TODO :type name: TODO :param challanges: A list of challenges from ACME "challenge" server message to be fulfilled by the client in order to prove possession of the identifier. :type challenges: list :param path: List of indices from `challenges`. :type path: list :returns: A pair of TODO :rtype: tuple """ sni_todo = [] # Since a single invocation of SNI challenge can satisfy multiple # challenges. We must keep track of all the challenges it satisfies sni_satisfies = [] challenge_objs = [] challenge_obj_indices = [] for index in path: chall = challenges[index] if chall["type"] == "dvsni": logger.info(" DVSNI challenge for name %s." % name) sni_satisfies.append(index) sni_todo.append((str(name), str(chall["r"]), str(chall["nonce"]))) elif chall["type"] == "recoveryToken": logger.info("\tRecovery Token Challenge for name: %s." % name) challenge_obj_indices.append(index) challenge_objs.append({ type: "recoveryToken", }) else: logger.fatal("Challenge not currently supported") sys.exit(82) if sni_todo: # SNI_Challenge can satisfy many sni challenges at once so only # one "challenge object" is issued for all sni_challenges challenge_objs.append({ "type": "dvsni", "listSNITuple": sni_todo, "dvsni_key": os.path.abspath(self.key_file), }) challenge_obj_indices.append(sni_satisfies) logger.debug(sni_todo) return challenge_objs, challenge_obj_indices
def send(self, json_obj): try: acme_object_validate(json.dumps(json_obj)) response = urllib2.urlopen( self.server_url, json.dumps(json_obj)).read() acme_object_validate(response) return json.loads(response) except: logger.fatal("Send() failed... may have lost connection to server") sys.exit(8)
def send(self, json_obj): try: acme_object_validate(json.dumps(json_obj)) response = urllib2.urlopen(self.server_url, json.dumps(json_obj)).read() acme_object_validate(response) return json.loads(response) except: logger.fatal("Send() failed... may have lost connection to server") sys.exit(8)
def sanity_check_names(names): """Make sure host names are valid. :param list names: List of host names """ for name in names: if not is_hostname_sane(name): logger.fatal(repr(name) + " is an impossible hostname") sys.exit(81)
def get_all_names(self): """Return all valid names in the configuration.""" names = list(self.config.get_all_names()) sanity_check_names(names) if not names: logger.fatal("No domain names were found in your apache config") logger.fatal("Either specify which names you would like " "letsencrypt to validate or add server names " "to your virtual hosts") sys.exit(1) return names
def handle_authorization(self, challenge_dict, chal_objs, responses): auth_dict = self.send(self.authorization_request( challenge_dict["sessionID"], self.names[0], challenge_dict["nonce"], responses)) try: return self.is_expected_msg(auth_dict, "authorization") except: logger.fatal("Failed Authorization procedure - \ cleaning up challenges") sys.exit(1) finally: self.cleanup_challenges(chal_objs)
def revert_challenge_config(self): """ This function should reload the users original configuration files for all saves with reversible=True """ if os.path.isdir(TEMP_CHECKPOINT_DIR): result = self.__recover_checkpoint(TEMP_CHECKPOINT_DIR) changes = True if result != 0: # We have a partial or incomplete recovery logger.fatal("Incomplete or failed recovery for %s" % TEMP_CHECKPOINT_DIR) sys.exit(67) # Remember to reload Augeas self.aug.load()
def send(self, json_obj): try: json_encoded = json.dumps(json_obj) acme_object_validate(json_encoded) response = requests.post( self.server_url, data=json_encoded, headers={"Content-Type": "application/json"}, ) body = response.content acme_object_validate(body) return response.json() except: logger.fatal("Send() failed... may have lost connection to server") sys.exit(8)
def handle_authorization(self, challenge_dict, chal_objs, responses): auth_dict = self.send( self.authorization_request(challenge_dict["sessionID"], self.names[0], challenge_dict["nonce"], responses)) try: return self.is_expected_msg(auth_dict, "authorization") except: logger.fatal("Failed Authorization procedure - \ cleaning up challenges") sys.exit(1) finally: self.cleanup_challenges(chal_objs)
def _find_smart_path(challenges, combos): """ Can be called if combinations is included Function uses a simple ranking system to choose the combo with the lowest cost :param challenges: A list of challenges from ACME "challenge" server message to be fulfilled by the client in order to prove possession of the identifier. :type challenges: list :param combos: A collection of sets of challenges from ACME "challenge" server message ("combinations"), each of which would be sufficient to prove possession of the identifier. :type combos: list or None :returns: List of indices from `challenges`. :rtype: list """ chall_cost = {} max_cost = 0 for i, chall in enumerate(CONFIG.CHALLENGE_PREFERENCES): chall_cost[chall] = i max_cost += i best_combo = [] # Set above completing all of the available challenges best_combo_cost = max_cost + 1 combo_total = 0 for combo in combos: for challenge_index in combo: combo_total += chall_cost.get(challenges[ challenge_index]["type"], max_cost) if combo_total < best_combo_cost: best_combo = combo best_combo_cost = combo_total combo_total = 0 if not best_combo: logger.fatal("Client does not support any combination of " "challenges to satisfy ACME server") sys.exit(22) return best_combo
def _find_smart_path(challenges, combos): """ Can be called if combinations is included Function uses a simple ranking system to choose the combo with the lowest cost :param challenges: A list of challenges from ACME "challenge" server message to be fulfilled by the client in order to prove possession of the identifier. :type challenges: list :param combos: A collection of sets of challenges from ACME "challenge" server message ("combinations"), each of which would be sufficient to prove possession of the identifier. :type combos: list or None :returns: List of indices from `challenges`. :rtype: list """ chall_cost = {} max_cost = 0 for i, chall in enumerate(CONFIG.CHALLENGE_PREFERENCES): chall_cost[chall] = i max_cost += i best_combo = [] # Set above completing all of the available challenges best_combo_cost = max_cost + 1 combo_total = 0 for combo in combos: for challenge_index in combo: combo_total += chall_cost.get(challenges[challenge_index]["type"], max_cost) if combo_total < best_combo_cost: best_combo = combo best_combo_cost = combo_total combo_total = 0 if not best_combo: logger.fatal("Client does not support any combination of " "challenges to satisfy ACME server") sys.exit(22) return best_combo
def revert_challenge_config(self): """Reload users original configuration files after a challenge. This function should reload the users original configuration files for all saves with temporary=True """ if os.path.isdir(CONFIG.TEMP_CHECKPOINT_DIR): result = self._recover_checkpoint(CONFIG.TEMP_CHECKPOINT_DIR) changes = True if result != 0: # We have a partial or incomplete recovery logger.fatal("Incomplete or failed recovery for " "%s" % CONFIG.TEMP_CHECKPOINT_DIR) sys.exit(67) # Remember to reload Augeas self.aug.load()
def get_key_csr_pem(self, csr_return_format='der'): """ Returns key and CSR using provided files or generating new files if necessary. Both will be saved in pem format on the filesystem. The CSR can optionally be returned in DER format as the CSR cannot be loaded back into M2Crypto. """ key_pem = None csr_pem = None if not self.key_file: key_pem = crypto_util.make_key(RSA_KEY_SIZE) # Save file le_util.make_or_verify_dir(KEY_DIR, 0700) key_f, self.key_file = le_util.unique_file( KEY_DIR + "key-letsencrypt.pem", 0600) key_f.write(key_pem) key_f.close() logger.info("Generating key: %s" % self.key_file) else: try: key_pem = open(self.key_file).read().replace("\r", "") except: logger.fatal("Unable to open key file: %s" % self.key_file) sys.exit(1) if not self.csr_file: csr_pem, csr_der = crypto_util.make_csr(self.key_file, self.names) # Save CSR le_util.make_or_verify_dir(CERT_DIR, 0755) csr_f, self.csr_file = le_util.unique_file( CERT_DIR + "csr-letsencrypt.pem", 0644) csr_f.write(csr_pem) csr_f.close() logger.info("Creating CSR: %s" % self.csr_file) else: #TODO fix this der situation try: csr_pem = open(self.csr_file).read().replace("\r", "") except: logger.fatal("Unable to open CSR file: %s" % self.csr_file) sys.exit(1) if csr_return_format == 'der': return key_pem, csr_der return key_pem, csr_pem
def get_key_csr_pem(self, csr_return_format = 'der'): """ Returns key and CSR using provided files or generating new files if necessary. Both will be saved in pem format on the filesystem. The CSR can optionally be returned in DER format as the CSR cannot be loaded back into M2Crypto. """ key_pem = None csr_pem = None if not self.key_file: key_pem = crypto_util.make_key(RSA_KEY_SIZE) # Save file le_util.make_or_verify_dir(KEY_DIR, 0700) key_f, self.key_file = le_util.unique_file( KEY_DIR + "key-letsencrypt.pem", 0600) key_f.write(key_pem) key_f.close() logger.info("Generating key: %s" % self.key_file) else: try: key_pem = open(self.key_file).read().replace("\r", "") except: logger.fatal("Unable to open key file: %s" % self.key_file) sys.exit(1) if not self.csr_file: csr_pem, csr_der = crypto_util.make_csr(self.key_file, self.names) # Save CSR le_util.make_or_verify_dir(CERT_DIR, 0755) csr_f, self.csr_file = le_util.unique_file( CERT_DIR + "csr-letsencrypt.pem", 0644) csr_f.write(csr_pem) csr_f.close() logger.info("Creating CSR: %s" % self.csr_file) else: #TODO fix this der situation try: csr_pem = open(self.csr_file).read().replace("\r", "") except: logger.fatal("Unable to open CSR file: %s" % self.csr_file) sys.exit(1) if csr_return_format == 'der': return key_pem, csr_der return key_pem, csr_pem
def recovery_routine(self): """ Revert all previously modified files. First, any changes found in TEMP_CHECKPOINT_DIR are removed, then IN_PROGRESS changes are removed The order is important. IN_PROGRESS is unable to add files that are already added by a TEMP change. Thus TEMP must be rolled back first because that will be the 'latest' occurrence of the file. """ self.revert_challenge_config() if os.path.isdir(IN_PROGRESS_DIR): result = self.__recover_checkpoint(IN_PROGRESS_DIR) if result != 0: # We have a partial or incomplete recovery # Not as egregious # TODO: Additional tests? recovery logger.fatal("Incomplete or failed recovery for %s" % IN_PROGRESS_DIR) sys.exit(68) # Need to reload configuration after these changes take effect self.aug.load()
def is_expected_msg(self, msg_dict, expected, delay=3, rounds = 20): for i in range(rounds): if msg_dict["type"] == expected: return msg_dict elif msg_dict["type"] == "error": logger.error("%s: %s - More Info: %s" % (msg_dict["error"], msg_dict.get("message", ""), msg_dict.get("moreInfo", ""))) raise Exception(msg_dict["error"]) elif msg_dict["type"] == "defer": logger.info("Waiting for %d seconds..." % delay) time.sleep(delay) msg_dict = self.send(self.status_request(msg_dict["token"])) else: logger.fatal("Received unexpected message") logger.fatal("Expected: %s" % expected) logger.fatal("Received: " + msg_dict) sys.exit(33) logger.error("Server has deferred past the max of %d seconds" % (rounds * delay)) return None
def __init__(self, ca_server, cert_signing_request=CSR(None, None, None), private_key=Key(None, None), use_curses=True): """Initialize client. :param str ca_server: Certificate authority server :param cert_signing_request: Certificate Signing Request :type cert_signing_request: :class:`CSR` :param private_key: Private key :type private_key: :class:`Key` :param bool use_curses: Use curses UI """ self.curses = use_curses # Logger needs to be initialized before Configurator self.init_logger() # TODO: Can probably figure out which configurator to use # without special packaging based on system info Command # line arg or client function to discover self.config = apache_configurator.ApacheConfigurator( CONFIG.SERVER_ROOT) self.server = ca_server # These are CSR/Key namedtuples self.csr = cert_signing_request self.privkey = private_key # TODO: Figure out all exceptions from this function try: self._validate_csr_key_cli() except errors.LetsEncryptClientError as exc: # TODO: Something nice here... logger.fatal(("%s - until the programmers get their act together, " "we are just going to exit" % str(exc))) sys.exit(1) self.server_url = "https://%s/acme/" % self.server
def _validate_csr_key_cli(self): """Validate CSR and key files. Verifies that the client key and csr arguments are valid and correspond to one another. """ # TODO: Handle all of these problems appropriately # The client can eventually do things like prompt the user # and allow the user to take more appropriate actions # If CSR is provided, the private key should also be provided. if self.csr_file and not self.key_file: logger.fatal(("Please provide the private key file used in " "generating the provided CSR")) sys.exit(1) # If CSR is provided, it must be readable and valid. try: if self.csr_file and not crypto_util.valid_csr(self.csr_file): raise Exception("The provided CSR is not a valid CSR") except IOError: raise Exception("The provided CSR could not be read") # If key is provided, it must be readable and valid. try: if self.key_file and not crypto_util.valid_privkey(self.key_file): raise Exception("The provided key is not a valid key") except IOError: raise Exception("The provided key could not be read") # If CSR and key are provided, the key must be the same key used # in the CSR. if self.csr_file and self.key_file: try: if not crypto_util.csr_matches_pubkey(self.csr_file, self.key_file): raise Exception("The key and CSR do not match") except IOError: raise Exception("The key or CSR files could not be read")
def _validate_csr_key_cli(self): """Validate CSR and key files. Verifies that the client key and csr arguments are valid and correspond to one another. """ # TODO: Handle all of these problems appropriately # The client can eventually do things like prompt the user # and allow the user to take more appropriate actions # If CSR is provided, the private key should also be provided. if self.csr_file and not self.key_file: logger.fatal(("Please provide the private key file used in " "generating the provided CSR")) sys.exit(1) # If CSR is provided, it must be readable and valid. try: if self.csr_file and not crypto_util.valid_csr(self.csr_file): raise Exception("The provided CSR is not a valid CSR") except IOError: raise Exception("The provided CSR could not be read") # If key is provided, it must be readable and valid. try: if self.key_file and not crypto_util.valid_privkey(self.key_file): raise Exception("The provided key is not a valid key") except IOError: raise Exception("The provided key could not be read") # If CSR and key are provided, the key must be the same key used # in the CSR. if self.csr_file and self.key_file: try: if not crypto_util.csr_matches_pubkey( self.csr_file, self.key_file): raise Exception("The key and CSR do not match") except IOError: raise Exception("The key or CSR files could not be read")
def recovery_routine(self): """Revert all previously modified files. First, any changes found in CONFIG.TEMP_CHECKPOINT_DIR are removed, then IN_PROGRESS changes are removed The order is important. IN_PROGRESS is unable to add files that are already added by a TEMP change. Thus TEMP must be rolled back first because that will be the 'latest' occurrence of the file. """ self.revert_challenge_config() if os.path.isdir(CONFIG.IN_PROGRESS_DIR): result = self._recover_checkpoint(CONFIG.IN_PROGRESS_DIR) if result != 0: # We have a partial or incomplete recovery # Not as egregious # TODO: Additional tests? recovery logger.fatal("Incomplete or failed recovery for %s" % CONFIG.IN_PROGRESS_DIR) sys.exit(68) # Need to reload configuration after these changes take effect self.aug.load()
def is_expected_msg(self, response, expected, delay=3, rounds=20): """Is reponse expected ACME message? :param response: ACME response message from server. :type response: dict :param expected: Name of the expected response ACME message type. :type expected: str :param delay: Number of seconds to delay before next round in case of ACME "defer" response message. :type delay: int :param rounds: Number of resend attempts in case of ACME "defer" reponse message. :type rounds: int :raises: Exception :returns: ACME response message from server. :rtype: dict """ for _ in xrange(rounds): if response["type"] == expected: return response elif response["type"] == "error": logger.error("%s: %s - More Info: %s" % (response["error"], response.get("message", ""), response.get("moreInfo", ""))) raise errors.LetsEncryptClientError(response["error"]) elif response["type"] == "defer": logger.info("Waiting for %d seconds..." % delay) time.sleep(delay) response = self.send(acme.status_request(response["token"])) else: logger.fatal("Received unexpected message") logger.fatal("Expected: %s" % expected) logger.fatal("Received: " + response) sys.exit(33) logger.error("Server has deferred past the max of %d seconds" % (rounds * delay))
def is_expected_msg(self, response, expected, delay=3, rounds=20): """Is reponse expected ACME message? :param response: ACME response message from server. :type response: dict :param expected: Name of the expected response ACME message type. :type expected: str :param delay: Number of seconds to delay before next round in case of ACME "defer" response message. :type delay: int :param rounds: Number of resend attempts in case of ACME "defer" reponse message. :type rounds: int :raises: Exception :returns: ACME response message from server. :rtype: dict """ for _ in xrange(rounds): if response["type"] == expected: return response elif response["type"] == "error": logger.error("%s: %s - More Info: %s" % (response["error"], response.get( "message", ""), response.get("moreInfo", ""))) raise errors.LetsEncryptClientError(response["error"]) elif response["type"] == "defer": logger.info("Waiting for %d seconds..." % delay) time.sleep(delay) response = self.send(acme.status_request(response["token"])) else: logger.fatal("Received unexpected message") logger.fatal("Expected: %s" % expected) logger.fatal("Received: " + response) sys.exit(33) logger.error("Server has deferred past the max of %d seconds" % (rounds * delay))
def is_expected_msg(self, msg_dict, expected, delay=3, rounds=20): for i in range(rounds): if msg_dict["type"] == expected: return msg_dict elif msg_dict["type"] == "error": logger.error("%s: %s - More Info: %s" % (msg_dict["error"], msg_dict.get( "message", ""), msg_dict.get("moreInfo", ""))) raise Exception(msg_dict["error"]) elif msg_dict["type"] == "defer": logger.info("Waiting for %d seconds..." % delay) time.sleep(delay) msg_dict = self.send(self.status_request(msg_dict["token"])) else: logger.fatal("Received unexpected message") logger.fatal("Expected: %s" % expected) logger.fatal("Received: " + msg_dict) sys.exit(33) logger.error("Server has deferred past the max of %d seconds" % (rounds * delay)) return None
import errno import os import pwd import stat import sys from letsencrypt.client import logger def make_or_verify_dir(directory, permissions=0755, uid=0): try: os.makedirs(directory, permissions) except OSError as exception: if exception.errno == errno.EEXIST: if not check_permissions(directory, permissions, uid): logger.fatal("%s exists and does not contain the proper permissions or owner" % directory) sys.exit(57) else: raise def check_permissions(filepath, mode, uid=0): file_stat = os.stat(filepath) if stat.S_IMODE(file_stat.st_mode) != mode: return False return file_stat.st_uid == uid def unique_file(default_name, mode = 0777): """ Safely finds a unique file for writing only (by default) """ count = 1
def save(self, title=None, temporary=False): """Saves all changes to the configuration files. This function first checks for save errors, if none are found, all configuration changes made will be saved. According to the function parameters. :param title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a timestamped directory. :type title: str :param temporary: Indicates whether the changes made will be quickly reversed in the future (ie. challenges) :type temporary: bool """ save_state = self.aug.get("/augeas/save") self.aug.set("/augeas/save", "noop") # Existing Errors ex_errs = self.aug.match("/augeas//error") try: # This is a noop save self.aug.save() except: # Check for the root of save problems new_errs = self.aug.match("/augeas//error") # logger.error("During Save - " + mod_conf) # Only print new errors caused by recent save for err in new_errs: if err not in ex_errs: logger.error("Unable to save file - " "%s" % err[13:len(err)-6]) logger.error("Attempted Save Notes") logger.error(self.save_notes) # Erase Save Notes self.save_notes = "" return False # Retrieve list of modified files # Note: Noop saves can cause the file to be listed twice, I used a # set to remove this possibility. This is a known augeas 0.10 error. save_paths = self.aug.match("/augeas/events/saved") # If the augeas tree didn't change, no files were saved and a backup # should not be created if save_paths: save_files = set() for p in save_paths: save_files.add(self.aug.get(p)[6:]) valid, message = self.check_tempfile_saves(save_files) if not valid: logger.fatal(message) # What is the protocol in this situation? # This shouldn't happen if the challenge codebase is correct return False # Create Checkpoint if temporary: self.add_to_checkpoint(CONFIG.TEMP_CHECKPOINT_DIR, save_files) else: self.add_to_checkpoint(CONFIG.IN_PROGRESS_DIR, save_files) if title and not temporary and os.path.isdir(CONFIG.IN_PROGRESS_DIR): success = self._finalize_checkpoint(CONFIG.IN_PROGRESS_DIR, title) if not success: # This should never happen # This will be hopefully be cleaned up on the recovery # routine startup sys.exit(9) self.aug.set("/augeas/save", save_state) self.save_notes = "" self.aug.save() return True
def save(self, title=None, temporary=False): """Saves all changes to the configuration files. This function is not transactional TODO: Instead rely on challenge to backup all files before modifications title: string - The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a timestamped directory. `title` has no effect if temporary is true. temporary: boolean - Indicates whether the changes made will be quickly reversed in the future (challenges) """ save_state = self.aug.get("/augeas/save") self.aug.set("/augeas/save", "noop") # Existing Errors ex_errs = self.aug.match("/augeas//error") try: # This is a noop save self.aug.save() except: # Check for the root of save problems new_errs = self.aug.match("/augeas//error") # logger.error("During Save - " + mod_conf) # Only print new errors caused by recent save for err in new_errs: if err not in ex_errs: logger.error("Unable to save file - " "%s" % err[13:len(err)-6]) logger.error("Attempted Save Notes") logger.error(self.save_notes) # Erase Save Notes self.save_notes = "" return False # Retrieve list of modified files # Note: Noop saves can cause the file to be listed twice, I used a # set to remove this possibility. This is a known augeas 0.10 error. save_paths = self.aug.match("/augeas/events/saved") # If the augeas tree didn't change, no files were saved and a backup # should not be created if save_paths: save_files = set() for p in save_paths: save_files.add(self.aug.get(p)[6:]) valid, message = self.check_tempfile_saves(save_files, temporary) if not valid: logger.fatal(message) # What is the protocol in this situation? # This shouldn't happen if the challenge codebase is correct return False # Create Checkpoint if temporary: self.add_to_checkpoint(CONFIG.TEMP_CHECKPOINT_DIR, save_files) else: self.add_to_checkpoint(CONFIG.IN_PROGRESS_DIR, save_files) if title and not temporary and os.path.isdir(CONFIG.IN_PROGRESS_DIR): success = self.__finalize_checkpoint(CONFIG.IN_PROGRESS_DIR, title) if not success: # This should never happen # This will be hopefully be cleaned up on the recovery # routine startup sys.exit(9) self.aug.set("/augeas/save", save_state) self.save_notes = "" self.aug.save() return True
def sanity_check_names(self, names): for name in names: if not self.is_hostname_sane(name): logger.fatal(repr(name) + " is an impossible hostname") sys.exit(81)
def sanity_check_names(names): for name in names: if not is_hostname_sane(name): logger.fatal(repr(name) + " is an impossible hostname") sys.exit(81)