def _restore_int(name, value): """Restores an integer key-value pair from a renewal config file. :param str name: option name :param str value: option value :returns: converted option value to be stored in the runtime config :rtype: int :raises errors.Error: if value can't be converted to an int """ if name == "http01_port" and value == "None": logger.info("updating legacy http01_port value") return cli.flag_default("http01_port") try: return int(value) except ValueError: raise errors.Error("Expected a numeric value for {0}".format(name))
def verify_cert_matches_priv_key(cert_path, key_path): """ Verifies that the private key and cert match. :param str cert_path: path to a cert in PEM format :param str key_path: path to a private key file :raises errors.Error: If they don't match. """ try: context = SSL.Context(SSL.SSLv23_METHOD) context.use_certificate_file(cert_path) context.use_privatekey_file(key_path) context.check_privatekey() except (IOError, SSL.Error) as e: error_str = "verifying the cert located at {0} matches the \ private key located at {1} has failed. \ Details: {2}".format(cert_path, key_path, e) logger.exception(error_str) raise errors.Error(error_str)
def _get_linode_client(self) -> '_LinodeLexiconClient': if not self.credentials: # pragma: no cover raise errors.Error("Plugin has not been prepared.") api_key = self.credentials.conf('key') api_version: Optional[Union[str, int]] = self.credentials.conf('version') if api_version == '': api_version = None if not api_version: api_version = 3 # Match for v4 api key regex_v4 = re.compile('^[0-9a-f]{64}$') regex_match = regex_v4.match(api_key) if regex_match: api_version = 4 else: api_version = int(api_version) return _LinodeLexiconClient(api_key, api_version)
def _avoid_invalidating_lineage(config, lineage, original_server): "Do not renew a valid cert with one from a staging server!" # Some lineages may have begun with --staging, but then had production certs # added to them with open(lineage.cert) as the_file: contents = the_file.read() latest_cert = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, contents) # all our test certs are from happy hacker fake CA, though maybe one day # we should test more methodically now_valid = "fake" not in repr(latest_cert.get_issuer()).lower() if util.is_staging(config.server): if not util.is_staging(original_server) or now_valid: if not config.break_my_certs: names = ", ".join(lineage.names()) raise errors.Error( "You've asked to renew/replace a seemingly valid certificate with " "a test certificate (domains: {0}). We will not do that " "unless you use the --break-my-certs flag!".format(names))
def parse_args(self): """Parses command line arguments and returns the result. :returns: parsed command line arguments :rtype: argparse.Namespace """ parsed_args = self.parser.parse_args(self.args) parsed_args.func = self.VERBS[self.verb] parsed_args.verb = self.verb if self.detect_defaults: return parsed_args # Do any post-parsing homework here if self.verb == "renew" and not parsed_args.dialog_mode: parsed_args.noninteractive_mode = True if parsed_args.staging or parsed_args.dry_run: self.set_test_server(parsed_args) if parsed_args.csr: self.handle_csr(parsed_args) if parsed_args.must_staple: parsed_args.staple = True # Avoid conflicting args conficting_args = ["quiet", "noninteractive_mode", "text_mode"] if parsed_args.dialog_mode: for arg in conficting_args: if getattr(parsed_args, arg): raise errors.Error( ("Conflicting values for displayer." " {0} conflicts with dialog_mode").format(arg) ) hooks.validate_hooks(parsed_args) return parsed_args
def deploy_certificate(self, domains: List[str], privkey_path: str, cert_path: str, chain_path: str, fullchain_path: str) -> None: """Install certificate :param list domains: list of domains to install the certificate :param str privkey_path: path to certificate private key :param str cert_path: certificate file path (optional) :param str fullchain_path: path to the full chain of the certificate :param str chain_path: chain file path """ if self.installer is None: logger.error("No installer specified, client is unable to deploy" "the certificate") raise errors.Error("No installer available") chain_path = None if chain_path is None else os.path.abspath( chain_path) display_util.notify("Deploying certificate") msg = "Could not install certificate" with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg): for dom in domains: self.installer.deploy_cert( domain=dom, cert_path=os.path.abspath(cert_path), key_path=os.path.abspath(privkey_path), chain_path=chain_path, fullchain_path=fullchain_path) self.installer.save() # needed by the Apache plugin self.installer.save("Deployed ACME Certificate") msg = ("We were unable to install your certificate, " "however, we successfully restored your " "server to its prior configuration.") with error_handler.ErrorHandler(self._rollback_and_restart, msg): # sites may have been enabled / final cleanup self.installer.restart()
def _handle_subset_cert_request(config, domains, cert): """Figure out what to do if a previous cert had a subset of the names now requested :param storage.RenewableCert cert: :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" :rtype: tuple """ existing = ", ".join(cert.names()) question = ( "You have an existing certificate that contains a portion of " "the domains you requested (ref: {0}){br}{br}It contains these " "names: {1}{br}{br}You requested these names for the new " "certificate: {2}.{br}{br}Do you want to expand and replace this existing " "certificate with the new certificate?").format( cert.configfile.filename, existing, ", ".join(domains), br=os.linesep) if config.expand or config.renew_by_default or zope.component.getUtility( interfaces.IDisplay).yesno(question, "Expand", "Cancel", cli_flag="--expand", force_interactive=True): return "renew", cert else: reporter_util = zope.component.getUtility(interfaces.IReporter) reporter_util.add_message( "To obtain a new certificate that contains these names without " "replacing your existing certificate for {0}, you must use the " "--duplicate option.{br}{br}" "For example:{br}{br}{1} --duplicate {2}".format(existing, sys.argv[0], " ".join( sys.argv[1:]), br=os.linesep), reporter_util.HIGH_PRIORITY) raise errors.Error(USER_CANCELLED)
def enhance_config(self, domains, chain_path): """Enhance the configuration. :param list domains: list of domains to configure :param chain_path: chain file path :type chain_path: `str` or `None` :raises .errors.Error: if no installer is specified in the client. """ if self.installer is None: logger.warning("No installer is specified, there isn't any " "configuration to enhance.") raise errors.Error("No installer available") enhanced = False enhancement_info = ( ("hsts", "ensure-http-header", "Strict-Transport-Security"), ("redirect", "redirect", None), ("staple", "staple-ocsp", chain_path), ("uir", "ensure-http-header", "Upgrade-Insecure-Requests"),) supported = self.installer.supported_enhancements() for config_name, enhancement_name, option in enhancement_info: config_value = getattr(self.config, config_name) if enhancement_name in supported: if config_name == "redirect" and config_value is None: config_value = enhancements.ask(enhancement_name) if config_value: self.apply_enhancement(domains, enhancement_name, option) enhanced = True elif config_value: logger.warning( "Option %s is not supported by the selected installer. " "Skipping enhancement.", config_name) msg = ("We were unable to restart web server") if enhanced: with error_handler.ErrorHandler(self._rollback_and_restart, msg): self.installer.restart()
def _handle_identical_cert_request(config, cert): """Figure out what to do if a cert has the same names as a previously obtained one :param storage.RenewableCert cert: :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal action can be: "newcert" | "renew" | "reinstall" :rtype: tuple """ if renewal.should_renew(config, cert): return "renew", cert if config.reinstall: # Set with --reinstall, force an identical certificate to be # reinstalled without further prompting. return "reinstall", cert question = ( "You have an existing certificate that contains exactly the same " "domains you requested and isn't close to expiry." "{br}(ref: {0}){br}{br}What would you like to do?").format( cert.configfile.filename, br=os.linesep) if config.verb == "run": keep_opt = "Attempt to reinstall this existing certificate" elif config.verb == "certonly": keep_opt = "Keep the existing certificate for now" choices = [keep_opt, "Renew & replace the cert (limit ~5 per 7 days)"] display = zope.component.getUtility(interfaces.IDisplay) response = display.menu(question, choices, "OK", "Cancel", default=0) if response[0] == display_util.CANCEL: # TODO: Add notification related to command-line options for # skipping the menu for this case. raise errors.Error("User chose to cancel the operation and may " "reinvoke the client.") elif response[1] == 0: return "reinstall", cert elif response[1] == 1: return "renew", cert else: assert False, "This is impossible"
def parse_preferred_challenges(pref_challs): """Translate and validate preferred challenges. :param pref_challs: list of preferred challenge types :type pref_challs: `list` of `str` :returns: validated list of preferred challenge types :rtype: `list` of `str` :raises errors.Error: if pref_challs is invalid """ aliases = {"dns": "dns-01", "http": "http-01"} challs = [c.strip() for c in pref_challs] challs = [aliases.get(c, c) for c in challs] unrecognized = ", ".join(name for name in challs if name not in challenges.Challenge.TYPES) if unrecognized: raise errors.Error("Unrecognized challenges: {0}".format(unrecognized)) return challs
def verify_renewable_cert_sig(renewable_cert): """ Verifies the signature of a `.storage.RenewableCert` object. :param `.storage.RenewableCert` renewable_cert: cert to verify :raises errors.Error: If signature verification fails. """ try: with open(renewable_cert.chain, 'rb') as chain: chain, _ = pyopenssl_load_certificate(chain.read()) with open(renewable_cert.cert, 'rb') as cert: cert = x509.load_pem_x509_certificate(cert.read(), default_backend()) hash_name = cert.signature_hash_algorithm.name OpenSSL.crypto.verify(chain, cert.signature, cert.tbs_certificate_bytes, hash_name) except (IOError, ValueError, OpenSSL.crypto.Error) as e: error_str = "verifying the signature of the cert located at {0} has failed. \ Details: {1}".format(renewable_cert.cert, e) logger.exception(error_str) raise errors.Error(error_str)
def verify_renewable_cert_sig(renewable_cert: interfaces.RenewableCert) -> None: """Verifies the signature of a RenewableCert object. :param renewable_cert: cert to verify :type renewable_cert: certbot.interfaces.RenewableCert :raises errors.Error: If signature verification fails. """ try: with open(renewable_cert.chain_path, 'rb') as chain_file: chain = x509.load_pem_x509_certificate(chain_file.read(), default_backend()) with open(renewable_cert.cert_path, 'rb') as cert_file: cert = x509.load_pem_x509_certificate(cert_file.read(), default_backend()) pk = chain.public_key() verify_signed_payload(pk, cert.signature, cert.tbs_certificate_bytes, cert.signature_hash_algorithm) except (IOError, ValueError, InvalidSignature) as e: error_str = "verifying the signature of the certificate located at {0} has failed. \ Details: {1}".format(renewable_cert.cert_path, e) logger.exception(error_str) raise errors.Error(error_str)
def challb_to_achall(challb: messages.ChallengeBody, account_key: josepy.JWK, domain: str) -> achallenges.AnnotatedChallenge: """Converts a ChallengeBody object to an AnnotatedChallenge. :param .ChallengeBody challb: ChallengeBody :param .JWK account_key: Authorized Account Key :param str domain: Domain of the challb :returns: Appropriate AnnotatedChallenge :rtype: :class:`certbot.achallenges.AnnotatedChallenge` """ chall = challb.chall logger.info("%s challenge for %s", chall.typ, domain) if isinstance(chall, challenges.KeyAuthorizationChallenge): return achallenges.KeyAuthorizationAnnotatedChallenge( challb=challb, domain=domain, account_key=account_key) elif isinstance(chall, challenges.DNS): return achallenges.DNS(challb=challb, domain=domain) raise errors.Error(f"Received unsupported challenge of type: {chall.typ}")
def _auth_from_domains(le_client, config, domains, lineage=None): """Authenticate and enroll certificate. :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal action can be: "newcert" | "renew" | "reinstall" """ # If lineage is specified, use that one instead of looking around for # a matching one. if lineage is None: # This will find a relevant matching lineage that exists action, lineage = _treat_as_renewal(config, domains) else: # Renewal, where we already know the specific lineage we're # interested in action = "renew" if action == "reinstall": # The lineage already exists; allow the caller to try installing # it without getting a new certificate at all. logger.info("Keeping the existing certificate") return "reinstall", lineage hooks.pre_hook(config) try: if action == "renew": logger.info("Renewing an existing certificate") renewal.renew_cert(config, domains, le_client, lineage) elif action == "newcert": # TREAT AS NEW REQUEST logger.info("Obtaining a new certificate") lineage = le_client.obtain_and_enroll_certificate(domains) if lineage is False: raise errors.Error("Certificate could not be obtained") finally: hooks.post_hook(config, final=False) if not config.dry_run and not config.verb == "renew": _report_new_cert(config, lineage.cert, lineage.fullchain) return action, lineage
def _auth_from_domains(le_client, config, domains, lineage=None): """Authenticate and enroll certificate.""" # Note: This can raise errors... caught above us though. This is now # a three-way case: reinstall (which results in a no-op here because # although there is a relevant lineage, we don't do anything to it # inside this function -- we don't obtain a new certificate), renew # (which results in treating the request as a renewal), or newcert # (which results in treating the request as a new certificate request). # If lineage is specified, use that one instead of looking around for # a matching one. if lineage is None: # This will find a relevant matching lineage that exists action, lineage = _treat_as_renewal(config, domains) else: # Renewal, where we already know the specific lineage we're # interested in action = "renew" if action == "reinstall": # The lineage already exists; allow the caller to try installing # it without getting a new certificate at all. return lineage, "reinstall" hooks.pre_hook(config) try: if action == "renew": renewal.renew_cert(config, domains, le_client, lineage) elif action == "newcert": # TREAT AS NEW REQUEST lineage = le_client.obtain_and_enroll_certificate(domains) if lineage is False: raise errors.Error("Certificate could not be obtained") finally: hooks.post_hook(config, final=False) if not config.dry_run and not config.verb == "renew": _report_new_cert(config, lineage.cert, lineage.fullchain) return lineage, action
def verify_signed_payload( public_key: Union[DSAPublicKey, 'Ed25519PublicKey', 'Ed448PublicKey', EllipticCurvePublicKey, RSAPublicKey], signature: bytes, payload: bytes, signature_hash_algorithm: hashes.HashAlgorithm) -> None: """Check the signature of a payload. :param RSAPublicKey/EllipticCurvePublicKey public_key: the public_key to check signature :param bytes signature: the signature bytes :param bytes payload: the payload bytes :param hashes.HashAlgorithm signature_hash_algorithm: algorithm used to hash the payload :raises InvalidSignature: If signature verification fails. :raises errors.Error: If public key type is not supported """ if isinstance(public_key, RSAPublicKey): public_key.verify(signature, payload, PKCS1v15(), signature_hash_algorithm) elif isinstance(public_key, EllipticCurvePublicKey): public_key.verify(signature, payload, ECDSA(signature_hash_algorithm)) else: raise errors.Error("Unsupported public key type.")
def raise_for_non_administrative_windows_rights(subcommand): """ On Windows, raise if current shell does not have the administrative rights. Do nothing on Linux. :param str subcommand: The subcommand (like 'certonly') passed to the certbot client. :raises .errors.Error: If the provided subcommand must be run on a shell with administrative rights, and current shell does not have these rights. """ # Why not simply try ctypes.windll.shell32.IsUserAnAdmin() and catch AttributeError ? # Because windll exists only on a Windows runtime, and static code analysis engines # do not like at all non existent objects when run from Linux (even if we handle properly # all the cases in the code). # So we access windll only by reflection to trick these engines. if hasattr(ctypes, 'windll') and subcommand not in UNPRIVILEGED_SUBCOMMANDS_ALLOWED: windll = getattr(ctypes, 'windll') if windll.shell32.IsUserAnAdmin() == 0: raise errors.Error( 'Error, "{0}" subcommand must be run on a shell with administrative rights.' .format(subcommand))
def import_csr_file(csrfile, data): """Import a CSR file, which can be either PEM or DER. :param str csrfile: CSR filename :param str data: contents of the CSR file :returns: (`OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, le_util.CSR object representing the CSR, list of domains requested in the CSR) :rtype: tuple """ for form, typ in (("der", OpenSSL.crypto.FILETYPE_ASN1,), ("pem", OpenSSL.crypto.FILETYPE_PEM,),): try: domains = get_names_from_csr(data, typ) except OpenSSL.crypto.Error: logger.debug("CSR parse error (form=%s, typ=%s):", form, typ) logger.debug(traceback.format_exc()) continue return typ, le_util.CSR(file=csrfile, data=data, form=form), domains raise errors.Error("Failed to parse CSR file: {0}".format(csrfile))
def _find_domains_or_certname(config, installer, question=None): """Retrieve domains and certname from config or user input. :param config: Configuration object :type config: interfaces.IConfig :param installer: Installer object :type installer: interfaces.IInstaller :param `str` question: Overriding dialog question to ask the user if asked to choose from domain names. :returns: Two-part tuple of domains and certname :rtype: `tuple` of list of `str` and `str` :raises errors.Error: Usage message, if parameters are not used correctly """ domains = None certname = config.certname # first, try to get domains from the config if config.domains: domains = config.domains # if we can't do that but we have a certname, get the domains # with that certname elif certname: domains = cert_manager.domains_for_certname(config, certname) # that certname might not have existed, or there was a problem. # try to get domains from the user. if not domains: domains = display_ops.choose_names(installer, question) if not domains and not certname: raise errors.Error("Please specify --domains, or --installer that " "will help in domain names autodiscovery, or " "--cert-name for an existing certificate name.") return domains, certname
def match_and_check_overlaps(cli_config: configuration.NamespaceConfig, acceptable_matches: Iterable[Union[ Callable[[storage.RenewableCert], str], Callable[[storage.RenewableCert], Optional[List[str]]]]], match_func: Callable[[storage.RenewableCert], str], rv_func: Callable[[storage.RenewableCert], str]) -> List[str]: """ Searches through all lineages for a match, and checks for duplicates. If a duplicate is found, an error is raised, as performing operations on lineages that have their properties incorrectly duplicated elsewhere is probably a bad idea. :param `configuration.NamespaceConfig` cli_config: parsed command line arguments :param list acceptable_matches: a list of functions that specify acceptable matches :param function match_func: specifies what to match :param function rv_func: specifies what to return """ def find_matches(candidate_lineage: storage.RenewableCert, return_value: List[str], acceptable_matches: Iterable[Union[ Callable[[storage.RenewableCert], str], Callable[[storage.RenewableCert], Optional[List[str]]]]]) -> List[str]: """Returns a list of matches using _search_lineages.""" acceptable_matches_resolved = [func(candidate_lineage) for func in acceptable_matches] acceptable_matches_rv: List[str] = [] for item in acceptable_matches_resolved: if isinstance(item, list): acceptable_matches_rv += item elif item: acceptable_matches_rv.append(item) match = match_func(candidate_lineage) if match in acceptable_matches_rv: return_value.append(rv_func(candidate_lineage)) return return_value matched: List[str] = _search_lineages(cli_config, find_matches, [], acceptable_matches) if not matched: raise errors.Error(f"No match found for cert-path {cli_config.cert_path}!") elif len(matched) > 1: raise errors.OverlappingMatchFound() return matched
def setup_log_file_handler(config, logfile, fmt): """Setup file debug logging.""" log_file_path = os.path.join(config.logs_dir, logfile) try: handler = logging.handlers.RotatingFileHandler(log_file_path, maxBytes=2**20, backupCount=10) except IOError as e: if e.errno == errno.EACCES: msg = ("Access denied writing to {0}. To run as non-root, set " + "--logs-dir, --config-dir, --work-dir to writable paths.") raise errors.Error(msg.format(log_file_path)) else: raise # rotate on each invocation, rollover only possible when maxBytes # is nonzero and backupCount is nonzero, so we set maxBytes as big # as possible not to overrun in single CLI invocation (1MB). handler.doRollover() # TODO: creates empty letsencrypt.log.1 file handler.setLevel(logging.DEBUG) handler_formatter = logging.Formatter(fmt=fmt) handler_formatter.converter = time.gmtime # don't use localtime handler.setFormatter(handler_formatter) return handler, log_file_path
def match_and_check_overlaps(cli_config, acceptable_matches, match_func, rv_func): """ Searches through all lineages for a match, and checks for duplicates. If a duplicate is found, an error is raised, as performing operations on lineages that have their properties incorrectly duplicated elsewhere is probably a bad idea. :param `configuration.NamespaceConfig` cli_config: parsed command line arguments :param list acceptable_matches: a list of functions that specify acceptable matches :param function match_func: specifies what to match :param function rv_func: specifies what to return """ def find_matches(candidate_lineage, return_value, acceptable_matches): """Returns a list of matches using _search_lineages.""" acceptable_matches = [ func(candidate_lineage) for func in acceptable_matches ] acceptable_matches_rv = [] # type: List[str] for item in acceptable_matches: if isinstance(item, list): acceptable_matches_rv += item else: acceptable_matches_rv.append(item) match = match_func(candidate_lineage) if match in acceptable_matches_rv: return_value.append(rv_func(candidate_lineage)) return return_value matched = _search_lineages(cli_config, find_matches, [], acceptable_matches) if not matched: raise errors.Error("No match found for cert-path {0}!".format( cli_config.cert_path[0])) elif len(matched) > 1: raise errors.OverlappingMatchFound() else: return matched
def _find_domains_or_certname(config, installer): """Retrieve domains and certname from config or user input. """ domains = None certname = config.certname # first, try to get domains from the config if config.domains: domains = config.domains # if we can't do that but we have a certname, get the domains # with that certname elif certname: domains = cert_manager.domains_for_certname(config, certname) # that certname might not have existed, or there was a problem. # try to get domains from the user. if not domains: domains = display_ops.choose_names(installer) if not domains and not certname: raise errors.Error("Please specify --domains, or --installer that " "will help in domain names autodiscovery, or " "--cert-name for an existing certificate name.") return domains, certname
def rename_lineage(config: configuration.NamespaceConfig) -> None: """Rename the specified lineage to the new name. :param config: Configuration. :type config: :class:`certbot._internal.configuration.NamespaceConfig` """ certname = get_certnames(config, "rename")[0] new_certname = config.new_certname if not new_certname: code, new_certname = display_util.input_text( "Enter the new name for certificate {0}".format(certname), force_interactive=True) if code != display_util.OK or not new_certname: raise errors.Error("User ended interaction.") lineage = lineage_for_certname(config, certname) if not lineage: raise errors.ConfigurationError("No existing certificate with name " "{0} found.".format(certname)) storage.rename_renewal_config(certname, new_certname, config) display_util.notification("Successfully renamed {0} to {1}." .format(certname, new_certname), pause=False)
def verify_renewable_cert_sig(renewable_cert): """Verifies the signature of a `.storage.RenewableCert` object. :param `.storage.RenewableCert` renewable_cert: cert to verify :raises errors.Error: If signature verification fails. """ try: with open(renewable_cert.chain, 'rb') as chain_file: # type: IO[bytes] chain = x509.load_pem_x509_certificate(chain_file.read(), default_backend()) with open(renewable_cert.cert, 'rb') as cert_file: # type: IO[bytes] cert = x509.load_pem_x509_certificate(cert_file.read(), default_backend()) pk = chain.public_key() with warnings.catch_warnings(): verify_signed_payload(pk, cert.signature, cert.tbs_certificate_bytes, cert.signature_hash_algorithm) except (IOError, ValueError, InvalidSignature) as e: error_str = "verifying the signature of the cert located at {0} has failed. \ Details: {1}".format(renewable_cert.cert, e) logger.exception(error_str) raise errors.Error(error_str)
def make_key(bits, cipher=None): """Generate PEM encoded RSA key. :param int bits: Number of bits, at least 1024. :param str passphrase: (optional) The passphrase to be used to encrypt the private key :returns: new RSA key in PEM form with specified number of bits :rtype: str """ assert bits >= 1024 # XXX key = OpenSSL.crypto.PKey() key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) #if cipher is None: #py27 tests workaround if not isinstance(cipher, str): return OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) else: try: return OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key, cipher, ask_for_passphrase) except ValueError: raise errors.Error("Invalid cipher supplied!")
def register(config, account_storage, tos_cb=None): """Register new account with an ACME CA. This function takes care of generating fresh private key, registering the account, optionally accepting CA Terms of Service and finally saving the account. It should be called prior to initialization of `Client`, unless account has already been created. :param certbot.configuration.NamespaceConfig config: Client configuration. :param .AccountStorage account_storage: Account storage where newly registered account will be saved to. Save happens only after TOS acceptance step, so any account private keys or `.RegistrationResource` will not be persisted if `tos_cb` returns ``False``. :param tos_cb: If ACME CA requires the user to accept a Terms of Service before registering account, client action is necessary. For example, a CLI tool would prompt the user acceptance. `tos_cb` must be a callable that should accept `.RegistrationResource` and return a `bool`: ``True`` iff the Terms of Service present in the contained `.Registration.terms_of_service` is accepted by the client, and ``False`` otherwise. ``tos_cb`` will be called only if the client action is necessary, i.e. when ``terms_of_service is not None``. This argument is optional, if not supplied it will default to automatic acceptance! :raises certbot.errors.Error: In case of any client problems, in particular registration failure, or unaccepted Terms of Service. :raises acme.errors.Error: In case of any protocol problems. :returns: Newly registered and saved account, as well as protocol API handle (should be used in `Client` initialization). :rtype: `tuple` of `.Account` and `acme.client.Client` """ # Log non-standard actions, potentially wrong API calls if account_storage.find_all(): logger.info("There are already existing accounts for %s", config.server) if config.email is None: if not config.register_unsafely_without_email: msg = ("No email was provided and " "--register-unsafely-without-email was not present.") logger.error(msg) raise errors.Error(msg) if not config.dry_run: logger.debug("Registering without email!") # If --dry-run is used, and there is no staging account, create one with no email. if config.dry_run: config.email = None # Each new registration shall use a fresh new key rsa_key = generate_private_key(public_exponent=65537, key_size=config.rsa_key_size, backend=default_backend()) key = jose.JWKRSA(key=jose.ComparableRSAKey(rsa_key)) acme = acme_from_config_key(config, key) # TODO: add phone? regr = perform_registration(acme, config, tos_cb) acc = account.Account(regr, key) account_storage.save(acc, acme) eff.prepare_subscription(config, acc) return acc, acme
def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-branches """Does the user want to delete their now-revoked certs? If run in non-interactive mode, deleting happens automatically, unless if both `--cert-name` and `--cert-path` were specified with conflicting values. :param config: parsed command line arguments :type config: interfaces.IConfig :returns: `None` :rtype: None :raises errors.Error: If anything goes wrong, including bad user input, if an overlapping archive dir is found for the specified lineage, etc ... """ display = zope.component.getUtility(interfaces.IDisplay) reporter_util = zope.component.getUtility(interfaces.IReporter) attempt_deletion = config.delete_after_revoke if attempt_deletion is None: msg = ("Would you like to delete the cert(s) you just revoked?") attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No", force_interactive=True, default=True) if not attempt_deletion: reporter_util.add_message("Not deleting revoked certs.", reporter_util.LOW_PRIORITY) return if not (config.certname or config.cert_path): raise errors.Error('At least one of --cert-path or --cert-name must be specified.') if config.certname and config.cert_path: # first, check if certname and cert_path imply the same certs implied_cert_name = cert_manager.cert_path_to_lineage(config) if implied_cert_name != config.certname: cert_path_implied_cert_name = cert_manager.cert_path_to_lineage(config) cert_path_implied_conf = storage.renewal_file_for_certname(config, cert_path_implied_cert_name) cert_path_cert = storage.RenewableCert(cert_path_implied_conf, config) cert_path_info = cert_manager.human_readable_cert_info(config, cert_path_cert, skip_filter_checks=True) cert_name_implied_conf = storage.renewal_file_for_certname(config, config.certname) cert_name_cert = storage.RenewableCert(cert_name_implied_conf, config) cert_name_info = cert_manager.human_readable_cert_info(config, cert_name_cert) msg = ("You specified conflicting values for --cert-path and --cert-name. " "Which did you mean to select?") choices = [cert_path_info, cert_name_info] try: code, index = display.menu(msg, choices, ok_label="Select", force_interactive=True) except errors.MissingCommandlineFlag: error_msg = ('To run in non-interactive mode, you must either specify only one of ' '--cert-path or --cert-name, or both must point to the same certificate lineages.') raise errors.Error(error_msg) if code != display_util.OK or not index in range(0, len(choices)): raise errors.Error("User ended interaction.") if index == 0: config.certname = cert_path_implied_cert_name else: config.cert_path = storage.cert_path_for_cert_name(config, config.certname) elif config.cert_path: config.certname = cert_manager.cert_path_to_lineage(config) else: # if only config.certname was specified config.cert_path = storage.cert_path_for_cert_name(config, config.certname) # don't delete if the archive_dir is used by some other lineage archive_dir = storage.full_archive_path( configobj.ConfigObj(storage.renewal_file_for_certname(config, config.certname)), config, config.certname) try: cert_manager.match_and_check_overlaps(config, [lambda x: archive_dir], lambda x: x.archive_dir, lambda x: x) except errors.OverlappingMatchFound: msg = ('Not deleting revoked certs due to overlapping archive dirs. More than ' 'one lineage is using {0}'.format(archive_dir)) reporter_util.add_message(''.join(msg), reporter_util.MEDIUM_PRIORITY) return except Exception as e: msg = ('config.default_archive_dir: {0}, config.live_dir: {1}, archive_dir: {2},' 'original exception: {3}') msg = msg.format(config.default_archive_dir, config.live_dir, archive_dir, e) raise errors.Error(msg) cert_manager.delete(config)
def _delete_if_appropriate(config): """Does the user want to delete their now-revoked certs? If run in non-interactive mode, deleting happens automatically. :param config: parsed command line arguments :type config: interfaces.IConfig :returns: `None` :rtype: None :raises errors.Error: If anything goes wrong, including bad user input, if an overlapping archive dir is found for the specified lineage, etc ... """ display = zope.component.getUtility(interfaces.IDisplay) reporter_util = zope.component.getUtility(interfaces.IReporter) attempt_deletion = config.delete_after_revoke if attempt_deletion is None: msg = ( "Would you like to delete the cert(s) you just revoked, along with all earlier and " "later versions of the cert?") attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No", force_interactive=True, default=True) if not attempt_deletion: reporter_util.add_message("Not deleting revoked certs.", reporter_util.LOW_PRIORITY) return # config.cert_path must have been set # config.certname may have been set assert config.cert_path if not config.certname: config.certname = cert_manager.cert_path_to_lineage(config) # don't delete if the archive_dir is used by some other lineage archive_dir = storage.full_archive_path( configobj.ConfigObj( storage.renewal_file_for_certname(config, config.certname)), config, config.certname) try: cert_manager.match_and_check_overlaps(config, [lambda x: archive_dir], lambda x: x.archive_dir, lambda x: x) except errors.OverlappingMatchFound: msg = ( 'Not deleting revoked certs due to overlapping archive dirs. More than ' 'one lineage is using {0}'.format(archive_dir)) reporter_util.add_message(''.join(msg), reporter_util.MEDIUM_PRIORITY) return except Exception as e: msg = ( 'config.default_archive_dir: {0}, config.live_dir: {1}, archive_dir: {2},' 'original exception: {3}') msg = msg.format(config.default_archive_dir, config.live_dir, archive_dir, e) raise errors.Error(msg) cert_manager.delete(config)
def enhance(config, plugins): """Add security enhancements to existing configuration :param config: Configuration object :type config: interfaces.IConfig :param plugins: List of plugins :type plugins: `list` of `str` :returns: `None` :rtype: None """ supported_enhancements = ["hsts", "redirect", "uir", "staple"] # Check that at least one enhancement was requested on command line oldstyle_enh = any( [getattr(config, enh) for enh in supported_enhancements]) if not enhancements.are_requested(config) and not oldstyle_enh: msg = ( "Please specify one or more enhancement types to configure. To list " "the available enhancement types, run:\n\n%s --help enhance\n") logger.warning(msg, sys.argv[0]) raise errors.MisconfigurationError( "No enhancements requested, exiting.") try: installer, _ = plug_sel.choose_configurator_plugins( config, plugins, "enhance") except errors.PluginSelectionError as e: return str(e) if not enhancements.are_supported(config, installer): raise errors.NotSupportedError( "One ore more of the requested enhancements " "are not supported by the selected installer") certname_question = ("Which certificate would you like to use to enhance " "your configuration?") config.certname = cert_manager.get_certnames( config, "enhance", allow_multiple=False, custom_prompt=certname_question)[0] cert_domains = cert_manager.domains_for_certname(config, config.certname) if config.noninteractive_mode: domains = cert_domains else: domain_question = ("Which domain names would you like to enable the " "selected enhancements for?") domains = display_ops.choose_values(cert_domains, domain_question) if not domains: raise errors.Error( "User cancelled the domain selection. No domains " "defined, exiting.") lineage = cert_manager.lineage_for_certname(config, config.certname) if not config.chain_path: config.chain_path = lineage.chain_path if oldstyle_enh: le_client = _init_le_client(config, authenticator=None, installer=installer) le_client.enhance_config(domains, config.chain_path, ask_redirect=False) if enhancements.are_requested(config): enhancements.enable(lineage, domains, installer, config) return None