././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3470836 certbot-2.9.0/0000775000175100017510000000000014561227516012171 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/CHANGELOG.md0000664000175100017510000035160214561227515014010 0ustar00ericaerica# Certbot change log Certbot adheres to [Semantic Versioning](https://semver.org/). ## 2.9.0 - 2024-02-08 ### Added * Support for Python 3.12 was added. ### Changed * ### Fixed * Updates `joinpath` syntax to only use one addition per call, because the multiple inputs version was causing mypy errors on Python 3.10. * Makes the `reconfigure` verb actually use the staging server for the dry run to check the new configuration. More details about these changes can be found on our GitHub repo. ## 2.8.0 - 2023-12-05 ### Added * Added support for [Alpine Linux](https://www.alpinelinux.org) distribution when is used the apache plugin ### Changed * Support for Python 3.7 was removed. ### Fixed * Stop using the deprecated `pkg_resources` API included in `setuptools`. More details about these changes can be found on our GitHub repo. ## 2.7.4 - 2023-11-01 ### Fixed * Fixed a bug introduced in version 2.7.0 that caused interactively entered webroot plugin values to not be saved for renewal. * Fixed a bug introduced in version 2.7.0 of our Lexicon based DNS plugins that caused them to fail to find the DNS zone that needs to be modified in some cases. More details about these changes can be found on our GitHub repo. ## 2.7.3 - 2023-10-24 ### Fixed * Fixed a bug where arguments with contained spaces weren't being handled correctly * Fixed a bug that caused the ACME account to not be properly restored on renewal causing problems in setups where the user had multiple accounts with the same ACME server. More details about these changes can be found on our GitHub repo. ## 2.7.2 - 2023-10-19 ### Fixed * `certbot-dns-ovh` plugin now requires `lexicon>=3.15.1` to ensure a consistent behavior with OVH APIs. * Fixed a bug where argument sources weren't correctly detected in abbreviated arguments, short arguments, and some other circumstances More details about these changes can be found on our GitHub repo. ## 2.7.1 - 2023-10-10 ### Fixed * Fixed a bug that broke the DNS plugin for DNSimple that was introduced in version 2.7.0 of the plugin. * Correctly specified the new minimum version of the ConfigArgParse package that Certbot requires which is 1.5.3. More details about these changes can be found on our GitHub repo. ## 2.7.0 - 2023-10-03 ### Added * Add `certbot.util.LooseVersion` class. See [GH #9489](https://github.com/certbot/certbot/issues/9489). * Add a new base class `certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator` to implement a DNS authenticator plugin backed by Lexicon to communicate with the provider DNS API. This approach relies heavily on conventions to reduce the implementation complexity of a new plugin. * Add a new test base class `certbot.plugins.dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest` to help testing DNS plugins implemented on top of `LexiconDNSAuthenticator`. ### Changed * `NamespaceConfig` now tracks how its arguments were set via a dictionary, allowing us to remove a bunch of global state previously needed to inspect whether a user set an argument or not. * Support for Python 3.7 was deprecated and will be removed in our next planned release. * Added `RENEWED_DOMAINS` and `FAILED_DOMAINS` environment variables for consumption by post renewal hooks. * Deprecates `LexiconClient` base class and `build_lexicon_config` function in `certbot.plugins.dns_common_lexicon` module in favor of `LexiconDNSAuthenticator`. * Deprecates `BaseLexiconAuthenticatorTest` and `BaseLexiconClientTest` test base classes of `certbot.plugins.dns_test_common_lexicon` module in favor of `BaseLexiconDNSAuthenticatorTest`. ### Fixed * Do not call deprecated datetime.utcnow() and datetime.utcfromtimestamp() * Filter zones in `certbot-dns-google` to avoid usage of private DNS zones to create records More details about these changes can be found on our GitHub repo. ## 2.6.0 - 2023-05-09 ### Added * `--dns-google-project` optionally allows for specifying the project that the DNS zone(s) reside in, which allows for Certbot usage in scenarios where the auth credentials reside in a different project to the zone(s) that are being managed. * There is now a new `Other` annotated challenge object to allow plugins to support entirely novel challenges. ### Changed * Optionally sign the SOA query for dns-rfc2136, to help resolve problems with split-view DNS setups and hidden primary setups. * Certbot versions prior to v1.32.0 did not sign queries with the specified TSIG key resulting in difficulty with split-horizon implementations. * Certbot v1.32.0 through v2.5.0 signed queries by default, potentially causing incompatibility with hidden primary setups with `allow-update-forwarding` enabled if the secondary did not also have the TSIG key within its config. * Certbot v2.6.0 and later no longer signs queries by default, but allows the user to optionally sign these queries by explicit configuration using the `dns_rfc2136_sign_query` option in the credentials .ini file. * Lineage name validity is performed for new lineages. `--cert-name` may no longer contain filepath separators (i.e. `/` or `\`, depending on the platform). * `certbot-dns-google` now loads credentials using the standard [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) strategy, rather than explicitly requiring the Google Compute metadata server to be present if a service account is not provided using `--dns-google-credentials`. * `--dns-google-credentials` now supports additional types of file-based credential, such as [External Account Credentials](https://google.aip.dev/auth/4117) created by Workload Identity Federation. All file-based credentials implemented by the Google Auth library are supported. ### Fixed * `certbot-dns-google` no longer requires deprecated `oauth2client` library. * Certbot will no longer try to invoke plugins which do not subclass from the proper `certbot.interfaces.{Installer,Authenticator}` interface (e.g. `certbot -i standalone` will now be ignored). See [GH-9664](https://github.com/certbot/certbot/issues/9664). More details about these changes can be found on our GitHub repo. ## 2.5.0 - 2023-04-04 ### Added * `acme.messages.OrderResource` now supports being round-tripped through JSON * acme.client.ClientV2 now provides separate `begin_finalization` and `poll_finalization` methods, in addition to the existing `finalize_order` method. ### Changed * `--dns-route53-propagation-seconds` is now deprecated. The Route53 plugin relies on the [GetChange API](https://docs.aws.amazon.com/Route53/latest/APIReference/API_GetChange.html) to determine if a DNS update is complete. The flag has never had any effect and will be removed in a future version of Certbot. * Packaged tests for all Certbot components besides josepy were moved inside the `_internal/tests` module. ### Fixed * Fixed `renew` sometimes not preserving the key type of RSA certificates. * Users who upgraded from Certbot =v2.0.0 may have had their RSA certificates inadvertently changed to ECDSA certificates. If desired, the key type may be changed back to RSA. See the [User Guide](https://eff-certbot.readthedocs.io/en/stable/using.html#changing-a-certificate-s-key-type). * Deprecated flags were inadvertently not printing warnings since v1.16.0. This is now fixed. More details about these changes can be found on our GitHub repo. ## 2.4.0 - 2023-03-07 ### Added * We deprecated support for the update_symlinks command. Support will be removed in a following version of Certbot. ### Changed * Docker build and deploy scripts now generate multiarch manifests for non-architecture-specific tags, instead of defaulting to amd64 images. ### Fixed * Reverted [#9475](https://github.com/certbot/certbot/pull/9475) due to a performance regression in large nginx deployments. More details about these changes can be found on our GitHub repo. ## 2.3.0 - 2023-02-14 ### Added * Allow a user to modify the configuration of a certificate without renewing it using the new `reconfigure` subcommand. See `certbot help reconfigure` for details. * `certbot show_account` now displays the [ACME Account Thumbprint](https://datatracker.ietf.org/doc/html/rfc8555#section-8.1). ### Changed * Certbot will no longer save previous CSRs and certificate private keys to `/etc/letsencrypt/csr` and `/etc/letsencrypt/keys`, respectively. These directories may be safely deleted. * Certbot will now only keep the current and 5 previous certificates in the `/etc/letsencrypt/archive` directory for each certificate lineage. Any prior certificates will be automatically deleted upon renewal. This number may be further lowered in future releases. * As always, users should only reference the certificate files within `/etc/letsencrypt/live` and never use `/etc/letsencrypt/archive` directly. See [Where are my certificates?](https://eff-certbot.readthedocs.io/en/stable/using.html#where-are-my-certificates) in the Certbot User Guide. * `certbot.configuration.NamespaceConfig.key_dir` and `.csr_dir` are now deprecated. * All Certbot components now require `pytest` to run tests. ### Fixed * Fixed a crash when registering an account with BuyPass' ACME server. * Fixed a bug where Certbot would crash with `AttributeError: can't set attribute` on ACME server errors in Python 3.11. See [GH #9539](https://github.com/certbot/certbot/issues/9539). More details about these changes can be found on our GitHub repo. ## 2.2.0 - 2023-01-11 ### Added * ### Changed * Certbot will no longer respect very long challenge polling intervals, which may be suggested by some ACME servers. Certbot will continue to wait up to 90 seconds by default, or up to a total of 30 minutes if requested by the server via `Retry-After`. ### Fixed * More details about these changes can be found on our GitHub repo. ## 1.32.2 - 2022-12-16 ### Fixed * Our snaps and Docker images were rebuilt to include updated versions of our dependencies. This release was not pushed to PyPI since those packages were unaffected. More details about these changes can be found on our GitHub repo. ## 2.1.1 - 2022-12-15 ### Fixed * Our snaps, Docker images, and Windows installer were rebuilt to include updated versions of our dependencies. This release was not pushed to PyPI since those packages were unaffected. More details about these changes can be found on our GitHub repo. ## 2.1.0 - 2022-12-07 ### Added * ### Changed * ### Fixed * Interfaces which plugins register themselves as implementing without inheriting from them now show up in `certbot plugins` output. * `IPluginFactory`, `IPlugin`, `IAuthenticator` and `IInstaller` have been re-added to `certbot.interfaces`. - This is to fix compatibility with a number of third-party DNS plugins which may have started erroring with `AttributeError` in Certbot v2.0.0. - Plugin authors can find more information about Certbot 2.x compatibility [here](https://github.com/certbot/certbot/wiki/Certbot-v2.x-Plugin-Compatibility). * A bug causing our certbot-apache tests to crash on some systems has been resolved. More details about these changes can be found on our GitHub repo. ## 1.32.1 - 2022-12-05 ### Fixed * Our snaps and docker images were rebuilt to include updated versions of our dependencies. This release was not pushed to PyPI since those packages were unaffected. More details about these changes can be found on our GitHub repo. ## 2.0.0 - 2022-11-21 ### Added * Support for Python 3.11 was added to Certbot and all of its components. * `acme.challenges.HTTP01Response.simple_verify` now accepts a timeout argument which defaults to 30 that causes the verification request to timeout after that many seconds. ### Changed * The default key type for new certificates is now ECDSA `secp256r1` (P-256). It was previously RSA 2048-bit. Existing certificates are not affected. * The Apache plugin no longer supports Apache 2.2. * `acme` and Certbot no longer support versions of ACME from before the RFC 8555 standard. * `acme` and Certbot no longer support the old `urn:acme:error:` ACME error prefix. * Removed the deprecated `certbot-dns-cloudxns` plugin. * Certbot will now error if a certificate has `--reuse-key` set and a conflicting `--key-type`, `--key-size` or `--elliptic-curve` is requested on the CLI. Use `--new-key` to change the key while preserving `--reuse-key`. * 3rd party plugins no longer support the `dist_name:plugin_name` format on the CLI and in configuration files. Use the shorter `plugin_name` format. * `acme.client.Client`, `acme.client.ClientBase`, `acme.client.BackwardsCompatibleClientV2`, `acme.mixins`, `acme.client.DER_CONTENT_TYPE`, `acme.fields.Resource`, `acme.fields.resource`, `acme.magic_typing`, `acme.messages.OLD_ERROR_PREFIX`, `acme.messages.Directory.register`, `acme.messages.Authorization.resolved_combinations`, `acme.messages.Authorization.combinations` have been removed. * `acme.messages.Directory` now only supports lookups by the exact resource name string in the ACME directory (e.g. `directory['newOrder']`). * Removed the deprecated `source_address` argument for `acme.client.ClientNetwork`. * The `zope` based interfaces in `certbot.interfaces` have been removed in favor of the `abc` based interfaces found in the same module. * Certbot no longer depends on `zope`. * Removed deprecated function `certbot.util.get_strict_version`. * Removed deprecated functions `certbot.crypto_util.init_save_csr`, `certbot.crypto_util.init_save_key`, and `certbot.compat.misc.execute_command` * The attributes `FileDisplay`, `NoninteractiveDisplay`, `SIDE_FRAME`, `input_with_timeout`, `separate_list_input`, `summarize_domain_list`, `HELP`, and `ESC` from `certbot.display.util` have been removed. * Removed deprecated functions `certbot.tests.util.patch_get_utility*`. Plugins should now patch `certbot.display.util` themselves in their tests or use `certbot.tests.util.patch_display_util` as a temporary workaround. * Certbot's test API under `certbot.tests` now uses `unittest.mock` instead of the 3rd party `mock` library. ### Fixed * Fixes a bug where the certbot working directory has unusably restrictive permissions on systems with stricter default umasks. * Requests to subscribe to the EFF mailing list now time out after 60 seconds. We plan to slowly roll out Certbot 2.0 to all of our snap users in the coming months. If you want to use the Certbot 2.0 snap now, please follow the instructions at https://community.letsencrypt.org/t/certbot-2-0-beta-call-for-testing/185945. More details about these changes can be found on our GitHub repo. ## 1.32.0 - 2022-11-08 ### Added * ### Changed * DNS RFC2136 module now uses the TSIG key to check for an authoritative SOA record. Helps the use of split-horizon and multiple views in BIND9 using the key in an ACL to determine which view to use. ### Fixed * CentOS 9 and other RHEL-derived OSes now correctly use httpd instead of apachectl for various Apache-related commands More details about these changes can be found on our GitHub repo. ## 1.31.0 - 2022-10-04 ### Added * ### Changed * If Certbot exits before setting up its usual log files, the temporary directory created to save logging information will begin with the name `certbot-log-` rather than a generic name. This should not be considered a [stable aspect of Certbot](https://certbot.eff.org/docs/compatibility.html) and may change again in the future. ### Fixed * Fixed an incompatibility in the certbot-dns-cloudflare plugin and the Cloudflare library which was introduced in the Cloudflare library version 2.10.1. The library would raise an error if a token was specified in the Certbot `--dns-cloudflare-credentials` file as well as the `cloudflare.cfg` configuration file of the Cloudflare library. More details about these changes can be found on our GitHub repo. ## 1.30.0 - 2022-09-07 ### Added * ### Changed * `acme.client.ClientBase`, `acme.messages.Authorization.resolved_combinations`, `acme.messages.Authorization.combinations`, `acme.mixins`, `acme.fields.resource`, and `acme.fields.Resource` are deprecated and will be removed in a future release. * `acme.messages.OLD_ERROR_PREFIX` (`urn:acme:error:`) is deprecated and support for the old ACME error prefix in Certbot will be removed in the next major release of Certbot. * `acme.messages.Directory.register` is deprecated and will be removed in the next major release of Certbot. Furthermore, `.Directory` will only support lookups by the exact resource name string in the ACME directory (e.g. `directory['newOrder']`). * The `certbot-dns-cloudxns` plugin is now deprecated and will be removed in the next major release of Certbot. * The `source_address` argument for `acme.client.ClientNetwork` is deprecated and support for it will be removed in the next major release. * Add UI text suggesting users create certs for multiple domains, when possible ### Fixed * More details about these changes can be found on our GitHub repo. ## 1.29.0 - 2022-07-05 ### Added * Updated Windows installer to be signed and trusted in Windows ### Changed * `--allow-subset-of-names` will now additionally retry in cases where domains are rejected while creating or finalizing orders. This requires subproblem support from the ACME server. ### Fixed * The `show_account` subcommand now uses the "newAccount" ACME endpoint to fetch the account data, so it doesn't rely on the locally stored account URL. This fixes situations where Certbot would use old ACMEv1 registration info with non-functional account URLs. * The generated Certificate Signing Requests are now generated as version 1 instead of version 3. This resolves situations in where strict enforcement of PKCS#10 meant that CSRs that were generated as version 3 were rejected. More details about these changes can be found on our GitHub repo. ## 1.28.0 - 2022-06-07 ### Added * Updated Apache/NGINX TLS configs to document contents are based on ssl-config.mozilla.org ### Changed * A change to order finalization has been made to the `acme` module and Certbot: - An order's `certificate` field will only be processed if the order's `status` is `valid`. - An order's `error` field will only be processed if the order's `status` is `invalid`. ### Fixed * More details about these changes can be found on our GitHub repo. ## 1.27.0 - 2022-05-03 ### Added * Added support for RFC8555 subproblems to our acme library. ### Changed * The PGP key `F2871B4152AE13C49519111F447BF683AA3B26C3` was added as an additional trusted key to sign our PyPI packages * When `certonly` is run with an installer specified (e.g. `--nginx`), `certonly` will now also run `restart` for that installer ### Fixed * More details about these changes can be found on our GitHub repo. ## 1.26.0 - 2022-04-05 ### Added * Added `--new-key`. When renewing or replacing a certificate that has `--reuse-key` set, it will force a new private key to be generated, one time. As before, `--reuse-key` and `--no-reuse-key` can be used to enable and disable key reuse. ### Changed * The default propagation timeout for the OVH DNS plugin (`--dns-ovh-propagation-seconds`) has been increased from 30 seconds to 120 seconds, based on user feedback. ### Fixed * Certbot for Windows has been upgraded to use Python 3.9.11, in response to https://www.openssl.org/news/secadv/20220315.txt. * Previously, when Certbot was in the process of registering a new ACME account and the ACME server did not present any Terms of Service, the user was asked to agree with a non-existent Terms of Service ("None"). This bug is now fixed, so that if an ACME server does not provide any Terms of Service to agree with, the user is not asked to agree to a non-existent Terms of Service any longer. * If account registration fails, Certbot did not relay the error from the ACME server back to the user. This is now fixed: the error message from the ACME server is now presented to the user when account registration fails. More details about these changes can be found on our GitHub repo. ## 1.25.0 - 2022-03-16 ### Added * ### Changed * Dropped 32 bit support for the Windows beta installer * Windows beta installer is now distributed as "certbot-beta-installer-win_amd64.exe". Users of the Windows beta should uninstall the old version before running this. * Added a check whether OCSP stapling is supported by the installer when requesting a certificate with the `run` subcommand in combination with the `--must-staple` option. If the installer does not support OCSP and the `--must-staple` option is used, Certbot will raise an error and quit. * Certbot and its acme module now depend on josepy>=1.13.0 due to better type annotation support. ### Fixed * Updated dependencies to use new version of cryptography that uses OpenSSL 1.1.1n, in response to https://www.openssl.org/news/secadv/20220315.txt. More details about these changes can be found on our GitHub repo. ## 1.24.0 - 2022-03-01 ### Added * When the `--debug-challenges` option is used in combination with `-v`, Certbot now displays the challenge URLs (for `http-01` challenges) or FQDNs (for `dns-01` challenges) and their expected return values. * ### Changed * Support for Python 3.6 was removed. * All Certbot components now require setuptools>=41.6.0. * The acme library now requires requests>=2.20.0. * Certbot and its acme library now require pytz>=2019.3. * certbot-nginx now requires pyparsing>=2.2.1. * certbot-dns-route53 now requires boto3>=1.15.15. ### Fixed * Nginx plugin now checks included files for the singleton server_names_hash_bucket_size directive. * More details about these changes can be found on our GitHub repo. ## 1.23.0 - 2022-02-08 ### Added * Added `show_account` subcommand, which will fetch the account information from the ACME server and show the account details (account URL and, if applicable, email address or addresses) * We deprecated support for Python 3.6 in Certbot and its ACME library. Support for Python 3.6 will be removed in the next major release of Certbot. ### Changed * ### Fixed * GCP Permission list for certbot-dns-google in plugin documentation * dns-digitalocean used the SOA TTL for newly created records, rather than 30 seconds. * Revoking a certificate based on an ECDSA key can now be done with `--key-path`. See [GH #8569](https://github.com/certbot/certbot/issues/8569). More details about these changes can be found on our GitHub repo. ## 1.22.0 - 2021-12-07 ### Added * Support for Python 3.10 was added to Certbot and all of its components. * The function certbot.util.parse_loose_version was added to parse version strings in the same way as the now deprecated distutils.version.LooseVersion class from the Python standard library. * Added `--issuance-timeout`. This option specifies how long (in seconds) Certbot will wait for the server to issue a certificate. ### Changed * The function certbot.util.get_strict_version was deprecated and will be removed in a future release. ### Fixed * Fixed an issue on Windows where the `web.config` created by Certbot would sometimes conflict with preexisting configurations (#9088). * Fixed an issue on Windows where the `webroot` plugin would crash when multiple domains had the same webroot. This affected Certbot 1.21.0. More details about these changes can be found on our GitHub repo. ## 1.21.0 - 2021-11-02 ### Added * Certbot will generate a `web.config` file on Windows in the challenge path when the `webroot` plugin is used, if one does not exist. This `web.config` file lets IIS serve challenge files while they do not have an extension. ### Changed * We changed the PGP key used to sign the packages we upload to PyPI. Going forward, releases will be signed with one of three different keys. All of these keys are available on major key servers and signed by our previous PGP key. The fingerprints of these new keys are: * BF6BCFC89E90747B9A680FD7B6029E8500F7DB16 * 86379B4F0AF371B50CD9E5FF3402831161D1D280 * 20F201346BF8F3F455A73F9A780CC99432A28621 ### Fixed * More details about these changes can be found on our GitHub repo. ## 1.20.0 - 2021-10-05 ### Added * Added `--no-reuse-key`. This remains the default behavior, but the flag may be useful to unset the `--reuse-key` option on existing certificates. ### Changed * ### Fixed * The certbot-dns-rfc2136 plugin in Certbot 1.19.0 inadvertently had an implicit dependency on `dnspython>=2.0`. This has been relaxed to `dnspython>=1.15.0`. More details about these changes can be found on our GitHub repo. ## 1.19.0 - 2021-09-07 ### Added * The certbot-dns-rfc2136 plugin always assumed the use of an IP address as the target server, but this was never checked. Until now. The plugin raises an error if the configured target server is not a valid IPv4 or IPv6 address. * Our acme library now supports requesting certificates for IP addresses. This feature is still unsupported by Certbot and Let's Encrypt. ### Changed * Several attributes in `certbot.display.util` module are deprecated and will be removed in a future release of Certbot. Any import of these attributes will emit a warning to prepare the transition for developers. * `zope` based interfaces in `certbot.interfaces` module are deprecated and will be removed in a future release of Certbot. Any import of these interfaces will emit a warning to prepare the transition for developers. * We removed the dependency on `chardet` from our acme library. Except for when downloading a certificate in an alternate format, our acme library now assumes all server responses are UTF-8 encoded which is required by RFC 8555. ### Fixed * Fixed parsing of `Define`d values in the Apache plugin to allow for `=` in the value. * Fixed a relatively harmless crash when issuing a certificate with `--quiet`/`-q`. More details about these changes can be found on our GitHub repo. ## 1.18.0 - 2021-08-03 ### Added * New functions that Certbot plugins can use to interact with the user have been added to `certbot.display.util`. We plan to deprecate using `IDisplay` with `zope` in favor of these new functions in the future. * The `Plugin`, `Authenticator` and `Installer` classes are added to `certbot.interfaces` module as alternatives to Certbot's current `zope` based plugin interfaces. The API of these interfaces is identical, but they are based on Python's `abc` module instead of `zope`. Certbot will continue to detect plugins that implement either interface, but we plan to drop support for `zope` based interfaces in a future version of Certbot. * The class `certbot.configuration.NamespaceConfig` is added to the Certbot's public API. ### Changed * When self-validating HTTP-01 challenges using acme.challenges.HTTP01Response.simple_verify, we now assume that the response is composed of only ASCII characters. Previously we were relying on the default behavior of the requests library which tries to guess the encoding of the response which was error prone. * `acme`: the `.client.Client` and `.client.BackwardsCompatibleClientV2` classes are now deprecated in favor of `.client.ClientV2`. * The `certbot.tests.patch_get_utility*` functions have been deprecated. Plugins should now patch `certbot.display.util` themselves in their tests or use `certbot.tests.util.patch_display_util` as a temporary workaround. * In order to simplify the transition to Certbot's new plugin interfaces, the classes `Plugin` and `Installer` in `certbot.plugins.common` module and `certbot.plugins.dns_common.DNSAuthenticator` now implement Certbot's new plugin interfaces. The Certbot plugins based on these classes are now automatically detected as implementing these interfaces. * We added a dependency on `chardet` to our acme library so that it will be used over `charset_normalizer` in newer versions of `requests`. ### Fixed * The Apache authenticator no longer crashes with "Unable to insert label" when encountering a completely empty vhost. This issue affected Certbot 1.17.0. * Users of the Certbot snap on Debian 9 (Stretch) should no longer encounter an "access denied" error when installing DNS plugins. More details about these changes can be found on our GitHub repo. ## 1.17.0 - 2021-07-06 ### Added * Add Void Linux overrides for certbot-apache. ### Changed * We changed how dependencies are specified between Certbot packages. For this and future releases, higher level Certbot components will require that lower level components are the same version or newer. More specifically, version X of the Certbot package will now always require acme>=X and version Y of a plugin package will always require acme>=Y and certbot=>Y. Specifying dependencies in this way simplifies testing and development. * The Apache authenticator now always configures virtual hosts which do not have an explicit `ServerName`. This should make it work more reliably with the default Apache configuration in Debian-based environments. ### Fixed * When we increased the logging level on our nginx "Could not parse file" message, it caused a previously-existing inability to parse empty files to become more visible. We have now added the ability to correctly parse empty files, so that message should only show for more significant errors. More details about these changes can be found on our GitHub repo. ## 1.16.0 - 2021-06-01 ### Added * ### Changed * DNS plugins based on lexicon now require dns-lexicon >= v3.1.0 * Use UTF-8 encoding for renewal configuration files * Windows installer now cleans up old Certbot dependency packages before installing the new ones to avoid version conflicts. * This release contains a substantial command-line UX overhaul, based on previous user research. The main goal was to streamline and clarify output. If you would like to see more verbose output, use the -v or -vv flags. UX improvements are an iterative process and the Certbot team welcomes constructive feedback. * Functions `certbot.crypto_util.init_save_key` and `certbot.crypto_util.init_save_csr`, whose behaviors rely on the global Certbot `config` singleton, are deprecated and will be removed in a future release. Please use `certbot.crypto_util.generate_key` and `certbot.crypto_util.generate_csr` instead. ### Fixed * Fix TypeError due to incompatibility with lexicon >= v3.6.0 * Installers (e.g. nginx, Apache) were being restarted unnecessarily after dry-run renewals. * Colors and bold text should properly render in all supported versions of Windows. More details about these changes can be found on our GitHub repo. ## 1.15.0 - 2021-05-04 ### Added * ### Changed * ### Fixed * More details about these changes can be found on our GitHub repo. ## 1.14.0 - 2021-04-06 ### Added * ### Changed * certbot-auto no longer checks for updates on any operating system. * The module `acme.magic_typing` is deprecated and will be removed in a future release. Please use the built-in module `typing` instead. * The DigitalOcean plugin now creates TXT records for the DNS-01 challenge with a lower 30s TTL. ### Fixed * Don't output an empty line for a hidden certificate when `certbot certificates` is being used in combination with `--cert-name` or `-d`. More details about these changes can be found on our GitHub repo. ## 1.13.0 - 2021-03-02 ### Added * ### Changed * CLI flags `--os-packages-only`, `--no-self-upgrade`, `--no-bootstrap` and `--no-permissions-check`, which are related to certbot-auto, are deprecated and will be removed in a future release. * Certbot no longer conditionally depends on an external mock module. Certbot's test API will continue to use it if it is available for backwards compatibility, however, this behavior has been deprecated and will be removed in a future release. * The acme library no longer depends on the `security` extras from `requests` which was needed to support SNI in TLS requests when using old versions of Python 2. * Certbot and all of its components no longer depend on the library `six`. * The update of certbot-auto itself is now disabled on all RHEL-like systems. * When revoking a certificate by `--cert-name`, it is no longer necessary to specify the `--server` if the certificate was obtained from a non-default ACME server. * The nginx authenticator now configures all matching HTTP and HTTPS vhosts for the HTTP-01 challenge. It is now compatible with external HTTPS redirection by a CDN or load balancer. ### Fixed * More details about these changes can be found on our GitHub repo. ## 1.12.0 - 2021-02-02 ### Added * ### Changed * The `--preferred-chain` flag now only checks the Issuer Common Name of the topmost (closest to the root) certificate in the chain, instead of checking every certificate in the chain. See [#8577](https://github.com/certbot/certbot/issues/8577). * Support for Python 2 has been removed. * In previous releases, we caused certbot-auto to stop updating its Certbot installation. In this release, we are beginning to disable updates to the certbot-auto script itself. This release includes Amazon Linux users, and all other systems that are not based on Debian or RHEL. We plan to make this change to the certbot-auto script for all users in the coming months. ### Fixed * Fixed the apache component on openSUSE Tumbleweed which no longer provides an apache2ctl symlink and uses apachectl instead. * Fixed a typo in `certbot/crypto_util.py` causing an error upon attempting `secp521r1` key generation More details about these changes can be found on our GitHub repo. ## 1.11.0 - 2021-01-05 ### Added * ### Changed * We deprecated support for Python 2 in Certbot and its ACME library. Support for Python 2 will be removed in the next planned release of Certbot. * certbot-auto was deprecated on all systems. For more information about this change, see https://community.letsencrypt.org/t/certbot-auto-no-longer-works-on-debian-based-systems/139702/7. * We deprecated support for Apache 2.2 in the certbot-apache plugin and it will be removed in a future release of Certbot. ### Fixed * The Certbot snap no longer loads packages installed via `pip install --user`. This was unintended and DNS plugins should be installed via `snap` instead. * `certbot-dns-google` would sometimes crash with HTTP 409/412 errors when used with very large zones. See [#6036](https://github.com/certbot/certbot/issues/6036). * `certbot-dns-google` would sometimes crash with an HTTP 412 error if preexisting records had an unexpected TTL, i.e.: different than Certbot's default TTL for this plugin. See [#8551](https://github.com/certbot/certbot/issues/8551). More details about these changes can be found on our GitHub repo. ## 1.10.1 - 2020-12-03 ### Fixed * Fixed a bug in `certbot.util.add_deprecated_argument` that caused the deprecated `--manual-public-ip-logging-ok` flag to crash Certbot in some scenarios. More details about these changes can be found on our GitHub repo. ## 1.10.0 - 2020-12-01 ### Added * Added timeout to DNS query function calls for dns-rfc2136 plugin. * Confirmation when deleting certificates * CLI flag `--key-type` has been added to specify 'rsa' or 'ecdsa' (default 'rsa'). * CLI flag `--elliptic-curve` has been added which takes an NIST/SECG elliptic curve. Any of `secp256r1`, `secp384r1` and `secp521r1` are accepted values. * The command `certbot certficates` lists the which type of the private key that was used for the private key. * Support for Python 3.9 was added to Certbot and all of its components. ### Changed * certbot-auto was deprecated on Debian based systems. * CLI flag `--manual-public-ip-logging-ok` is now a no-op, generates a deprecation warning, and will be removed in a future release. ### Fixed * Fixed a Unicode-related crash in the nginx plugin when running under Python 2. More details about these changes can be found on our GitHub repo. ## 1.9.0 - 2020-10-06 ### Added * `--preconfigured-renewal` flag, for packager use only. See the [packaging guide](https://certbot.eff.org/docs/packaging.html). ### Changed * certbot-auto was deprecated on all systems except for those based on Debian or RHEL. * Update the packaging instructions to promote usage of `python -m pytest` to test Certbot instead of the deprecated `python setup.py test` setuptools approach. * Reduced CLI logging when reloading nginx, if it is not running. * Reduced CLI logging when handling some kinds of errors. ### Fixed * Fixed `server_name` case-sensitivity in the nginx plugin. * The minimum version of the `acme` library required by Certbot was corrected. In the previous release, Certbot said it required `acme>=1.6.0` when it actually required `acme>=1.8.0` to properly support removing contact information from an ACME account. * Upgraded the version of httplib2 used in our snaps and Docker images to add support for proxy environment variables and fix the plugin for Google Cloud DNS. More details about these changes can be found on our GitHub repo. ## 1.8.0 - 2020-09-08 ### Added * Added the ability to remove email and phone contact information from an account using `update_account --register-unsafely-without-email` ### Changed * Support for Python 3.5 has been removed. ### Fixed * The problem causing the Apache plugin in the Certbot snap on ARM systems to fail to load the Augeas library it depends on has been fixed. * The `acme` library can now tell the ACME server to clear contact information by passing an empty `tuple` to the `contact` field of a `Registration` message. * Fixed the `*** stack smashing detected ***` error in the Certbot snap on some systems. More details about these changes can be found on our GitHub repo. ## 1.7.0 - 2020-08-04 ### Added * Third-party plugins can be used without prefix (`plugin_name` instead of `dist_name:plugin_name`): this concerns the plugin name, CLI flags, and keys in credential files. The prefixed form is still supported but is deprecated, and will be removed in a future release. * Added `--nginx-sleep-seconds` (default `1`) for environments where nginx takes a long time to reload. ### Changed * The Linode DNS plugin now waits 120 seconds for DNS propagation, instead of 1200, due to https://www.linode.com/blog/linode/linode-turns-17/ * We deprecated support for Python 3.5 in Certbot and its ACME library. Support for Python 3.5 will be removed in the next major release of Certbot. ### Fixed More details about these changes can be found on our GitHub repo. ## 1.6.0 - 2020-07-07 ### Added * Certbot snaps are now available for the arm64 and armhf architectures. * Add minimal code to run Nginx plugin on NetBSD. * Make Certbot snap find externally snapped plugins * Function `certbot.compat.filesystem.umask` is a drop-in replacement for `os.umask` implementing umask for both UNIX and Windows systems. * Support for alternative certificate chains in the `acme` module. * Added `--preferred-chain `. If a CA offers multiple certificate chains, it may be used to indicate to Certbot which chain should be preferred. * e.g. `--preferred-chain "DST Root CA X3"` ### Changed * Allow session tickets to be disabled in Apache when mod_ssl is statically linked. * Generalize UI warning message on renewal rate limits * Certbot behaves similarly on Windows to on UNIX systems regarding umask, and the umask `022` is applied by default: all files/directories are not writable by anyone other than the user running Certbot and the system/admin users. * Read acmev1 Let's Encrypt server URL from renewal config as acmev2 URL to prepare for impending acmev1 deprecation. ### Fixed * Cloudflare API Tokens may now be restricted to individual zones. * Don't use `StrictVersion`, but `LooseVersion` to check version requirements with setuptools, to fix some packaging issues with libraries respecting PEP404 for version string, with doesn't match `StrictVersion` requirements. * Certbot output doesn't refer to SSL Labs due to confusing scoring behavior. * Fix paths when calling to programs outside of the Certbot Snap, fixing the apache and nginx plugins on, e.g., CentOS 7. More details about these changes can be found on our GitHub repo. ## 1.5.0 - 2020-06-02 ### Added * Require explicit confirmation of snap plugin permissions before connecting. ### Changed * Improved error message in apache installer when mod_ssl is not available. ### Fixed * Add support for OCSP responses which use a public key hash ResponderID, fixing interoperability with Sectigo CAs. * Fix TLS-ALPN test that fails when run with newer versions of OpenSSL. More details about these changes can be found on our GitHub repo. ## 1.4.0 - 2020-05-05 ### Added * Turn off session tickets for apache plugin by default when appropriate. * Added serial number of certificate to the output of `certbot certificates` * Expose two new environment variables in the authenticator and cleanup scripts used by the `manual` plugin: `CERTBOT_REMAINING_CHALLENGES` is equal to the number of challenges remaining after the current challenge, `CERTBOT_ALL_DOMAINS` is a comma-separated list of all domains challenged for the current certificate. * Added TLS-ALPN-01 challenge support in the `acme` library. Support of this challenge in the Certbot client is planned to be added in a future release. * Added minimal proxy support for OCSP verification. * On Windows, hooks are now executed in a Powershell shell instead of a CMD shell, allowing both `*.ps1` and `*.bat` as valid scripts for Certbot. ### Changed * Reorganized error message when a user entered an invalid email address. * Stop asking interactively if the user would like to add a redirect. * `mock` dependency is now conditional on Python 2 in all of our packages. * Deprecate certbot-auto on Gentoo, macOS, and FreeBSD. * Allow existing but empty archive and live dir to be used when creating new lineage. ### Fixed * When using an RFC 8555 compliant endpoint, the `acme` library no longer sends the `resource` field in any requests or the `type` field when responding to challenges. * Fix nginx plugin crash when non-ASCII configuration file is being read (instead, the user will be warned that UTF-8 must be used). * Fix hanging OCSP queries during revocation checking - added a 10 second timeout. * Standalone servers now have a default socket timeout of 30 seconds, fixing cases where an idle connection can cause the standalone plugin to hang. * Parsing of the RFC 8555 application/pem-certificate-chain now tolerates CRLF line endings. This should fix interoperability with Buypass' services. More details about these changes can be found on our GitHub repo. ## 1.3.0 - 2020-03-03 ### Added * Added certbot.ocsp Certbot's API. The certbot.ocsp module can be used to determine the OCSP status of certificates. * Don't verify the existing certificate in HTTP01Response.simple_verify, for compatibility with the real-world ACME challenge checks. * Added support for `$hostname` in nginx `server_name` directive ### Changed * Certbot will now renew certificates early if they have been revoked according to OCSP. * Fix acme module warnings when response Content-Type includes params (e.g. charset). * Fixed issue where webroot plugin would incorrectly raise `Read-only file system` error when creating challenge directories (issue #7165). ### Fixed * Fix Apache plugin to use less restrictive umask for making the challenge directory when a restrictive umask was set when certbot was started. More details about these changes can be found on our GitHub repo. ## 1.2.0 - 2020-02-04 ### Added * Added support for Cloudflare's limited-scope API Tokens ### Changed * Add directory field to error message when field is missing. * If MD5 hasher is not available, try it in non-security mode (fix for FIPS systems) -- [#1948](https://github.com/certbot/certbot/issues/1948) * Disable old SSL versions and ciphersuites and remove `SSLCompression off` setting to follow Mozilla recommendations in Apache. * Remove ECDHE-RSA-AES128-SHA from NGINX ciphers list now that Windows 2008 R2 and Windows 7 are EOLed * Support for Python 3.4 has been removed. ### Fixed * Fix collections.abc imports for Python 3.9. More details about these changes can be found on our GitHub repo. ## 1.1.0 - 2020-01-14 ### Added * ### Changed * Removed the fallback introduced with 0.34.0 in `acme` to retry a POST-as-GET request as a GET request when the targeted ACME CA server seems to not support POST-as-GET requests. * certbot-auto no longer supports architectures other than x86_64 on RHEL 6 based systems. Existing certbot-auto installations affected by this will continue to work, but they will no longer receive updates. To install a newer version of Certbot on these systems, you should update your OS. * Support for Python 3.4 in Certbot and its ACME library is deprecated and will be removed in the next release of Certbot. certbot-auto users on x86_64 systems running RHEL 6 or derivatives will be asked to enable Software Collections (SCL) repository so Python 3.6 can be installed. certbot-auto can enable the SCL repo for you on CentOS 6 while users on other RHEL 6 based systems will be asked to do this manually. ### Fixed * More details about these changes can be found on our GitHub repo. ## 1.0.0 - 2019-12-03 ### Added * ### Removed * The `docs` extras for the `certbot-apache` and `certbot-nginx` packages have been removed. ### Changed * certbot-auto has deprecated support for systems using OpenSSL 1.0.1 that are not running on x86-64. This primarily affects RHEL 6 based systems. * Certbot's `config_changes` subcommand has been removed * `certbot.plugins.common.TLSSNI01` has been removed. * Deprecated attributes related to the TLS-SNI-01 challenge in `acme.challenges` and `acme.standalone` have been removed. * The functions `certbot.client.view_config_changes`, `certbot.main.config_changes`, `certbot.plugins.common.Installer.view_config_changes`, `certbot.reverter.Reverter.view_config_changes`, and `certbot.util.get_systemd_os_info` have been removed * Certbot's `register --update-registration` subcommand has been removed * When possible, default to automatically configuring the webserver so all requests redirect to secure HTTPS access. This is mostly relevant when running Certbot in non-interactive mode. Previously, the default was to not redirect all requests. ### Fixed * More details about these changes can be found on our GitHub repo. ## 0.40.1 - 2019-11-05 ### Changed * Added back support for Python 3.4 to Certbot components and certbot-auto due to a bug when requiring Python 2.7 or 3.5+ on RHEL 6 based systems. More details about these changes can be found on our GitHub repo. ## 0.40.0 - 2019-11-05 ### Added * ### Changed * We deprecated support for Python 3.4 in Certbot and its ACME library. Support for Python 3.4 will be removed in the next major release of Certbot. certbot-auto users on RHEL 6 based systems will be asked to enable Software Collections (SCL) repository so Python 3.6 can be installed. certbot-auto can enable the SCL repo for you on CentOS 6 while users on other RHEL 6 based systems will be asked to do this manually. * `--server` may now be combined with `--dry-run`. Certbot will, as before, use the staging server instead of the live server when `--dry-run` is used. * `--dry-run` now requests fresh authorizations every time, fixing the issue where it was prone to falsely reporting success. * Updated certbot-dns-google to depend on newer versions of google-api-python-client and oauth2client. * The OS detection logic again uses distro library for Linux OSes * certbot.plugins.common.TLSSNI01 has been deprecated and will be removed in a future release. * CLI flags --tls-sni-01-port and --tls-sni-01-address have been removed. * The values tls-sni and tls-sni-01 for the --preferred-challenges flag are no longer accepted. * Removed the flags: `--agree-dev-preview`, `--dialog`, and `--apache-init-script` * acme.standalone.BaseRequestHandlerWithLogging and acme.standalone.simple_tls_sni_01_server have been deprecated and will be removed in a future release of the library. * certbot-dns-rfc2136 now use TCP to query SOA records. ### Fixed * More details about these changes can be found on our GitHub repo. ## 0.39.0 - 2019-10-01 ### Added * Support for Python 3.8 was added to Certbot and all of its components. * Support for CentOS 8 was added to certbot-auto. ### Changed * Don't send OCSP requests for expired certificates * Return to using platform.linux_distribution instead of distro.linux_distribution in OS fingerprinting for Python < 3.8 * Updated the Nginx plugin's TLS configuration to keep support for some versions of IE11. ### Fixed * Fixed OS detection in the Apache plugin on RHEL 6. More details about these changes can be found on our GitHub repo. ## 0.38.0 - 2019-09-03 ### Added * Disable session tickets for Nginx users when appropriate. ### Changed * If Certbot fails to rollback your server configuration, the error message links to the Let's Encrypt forum. Change the link to the Help category now that the Server category has been closed. * Replace platform.linux_distribution with distro.linux_distribution as a step towards Python 3.8 support in Certbot. ### Fixed * Fixed OS detection in the Apache plugin on Scientific Linux. More details about these changes can be found on our GitHub repo. ## 0.37.2 - 2019-08-21 * Stop disabling TLS session tickets in Nginx as it caused TLS failures on some systems. More details about these changes can be found on our GitHub repo. ## 0.37.1 - 2019-08-08 ### Fixed * Stop disabling TLS session tickets in Apache as it caused TLS failures on some systems. More details about these changes can be found on our GitHub repo. ## 0.37.0 - 2019-08-07 ### Added * Turn off session tickets for apache plugin by default * acme: Authz deactivation added to `acme` module. ### Changed * Follow updated Mozilla recommendations for Nginx ssl_protocols, ssl_ciphers, and ssl_prefer_server_ciphers ### Fixed * Fix certbot-auto failures on RHEL 8. More details about these changes can be found on our GitHub repo. ## 0.36.0 - 2019-07-11 ### Added * Turn off session tickets for nginx plugin by default * Added missing error types from RFC8555 to acme ### Changed * Support for Ubuntu 14.04 Trusty has been removed. * Update the 'manage your account' help to be more generic. * The error message when Certbot's Apache plugin is unable to modify your Apache configuration has been improved. * Certbot's config_changes subcommand has been deprecated and will be removed in a future release. * `certbot config_changes` no longer accepts a --num parameter. * The functions `certbot.plugins.common.Installer.view_config_changes` and `certbot.reverter.Reverter.view_config_changes` have been deprecated and will be removed in a future release. ### Fixed * Replace some unnecessary platform-specific line separation. More details about these changes can be found on our GitHub repo. ## 0.35.1 - 2019-06-10 ### Fixed * Support for specifying an authoritative base domain in our dns-rfc2136 plugin has been removed. This feature was added in our last release but had a bug which caused the plugin to fail so the feature has been removed until it can be added properly. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * certbot-dns-rfc2136 More details about these changes can be found on our GitHub repo. ## 0.35.0 - 2019-06-05 ### Added * dns_rfc2136 plugin now supports explicitly specifying an authoritative base domain for cases when the automatic method does not work (e.g. Split horizon DNS) ### Changed * ### Fixed * Renewal parameter `webroot_path` is always saved, avoiding some regressions when `webroot` authenticator plugin is invoked with no challenge to perform. * Certbot now accepts OCSP responses when an explicit authorized responder, different from the issuer, is used to sign OCSP responses. * Scripts in Certbot hook directories are no longer executed when their filenames end in a tilde. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * certbot * certbot-dns-rfc2136 More details about these changes can be found on our GitHub repo. ## 0.34.2 - 2019-05-07 ### Fixed * certbot-auto no longer writes a check_permissions.py script at the root of the filesystem. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only changes in this release were to certbot-auto. More details about these changes can be found on our GitHub repo. ## 0.34.1 - 2019-05-06 ### Fixed * certbot-auto no longer prints a blank line when there are no permissions problems. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only changes in this release were to certbot-auto. More details about these changes can be found on our GitHub repo. ## 0.34.0 - 2019-05-01 ### Changed * Apache plugin now tries to restart httpd on Fedora using systemctl if a configuration test error is detected. This has to be done due to the way Fedora now generates the self signed certificate files upon first restart. * Updated Certbot and its plugins to improve the handling of file system permissions on Windows as a step towards adding proper Windows support to Certbot. * Updated urllib3 to 1.24.2 in certbot-auto. * Removed the fallback introduced with 0.32.0 in `acme` to retry a challenge response with a `keyAuthorization` if sending the response without this field caused a `malformed` error to be received from the ACME server. * Linode DNS plugin now supports api keys created from their new panel at [cloud.linode.com](https://cloud.linode.com) ### Fixed * Fixed Google DNS Challenge issues when private zones exist * Adding a warning noting that future versions of Certbot will automatically configure the webserver so that all requests redirect to secure HTTPS access. You can control this behavior and disable this warning with the --redirect and --no-redirect flags. * certbot-auto now prints warnings when run as root with insecure file system permissions. If you see these messages, you should fix the problem by following the instructions at https://community.letsencrypt.org/t/certbot-auto-deployment-best-practices/91979/, however, these warnings can be disabled as necessary with the flag --no-permissions-check. * `acme` module uses now a POST-as-GET request to retrieve the registration from an ACME v2 server * Convert the tsig algorithm specified in the certbot_dns_rfc2136 configuration file to all uppercase letters before validating. This makes the value in the config case insensitive. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * acme * certbot * certbot-apache * certbot-dns-cloudflare * certbot-dns-cloudxns * certbot-dns-digitalocean * certbot-dns-dnsimple * certbot-dns-dnsmadeeasy * certbot-dns-gehirn * certbot-dns-google * certbot-dns-linode * certbot-dns-luadns * certbot-dns-nsone * certbot-dns-ovh * certbot-dns-rfc2136 * certbot-dns-route53 * certbot-dns-sakuracloud * certbot-nginx More details about these changes can be found on our GitHub repo. ## 0.33.1 - 2019-04-04 ### Fixed * A bug causing certbot-auto to print warnings or crash on some RHEL based systems has been resolved. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only changes in this release were to certbot-auto. More details about these changes can be found on our GitHub repo. ## 0.33.0 - 2019-04-03 ### Added * Fedora 29+ is now supported by certbot-auto. Since Python 2.x is on a deprecation path in Fedora, certbot-auto will install and use Python 3.x on Fedora 29+. * CLI flag `--https-port` has been added for Nginx plugin exclusively, and replaces `--tls-sni-01-port`. It defines the HTTPS port the Nginx plugin will use while setting up a new SSL vhost. By default the HTTPS port is 443. ### Changed * Support for TLS-SNI-01 has been removed from all official Certbot plugins. * Attributes related to the TLS-SNI-01 challenge in `acme.challenges` and `acme.standalone` modules are deprecated and will be removed soon. * CLI flags `--tls-sni-01-port` and `--tls-sni-01-address` are now no-op, will generate a deprecation warning if used, and will be removed soon. * Options `tls-sni` and `tls-sni-01` in `--preferred-challenges` flag are now no-op, will generate a deprecation warning if used, and will be removed soon. * CLI flag `--standalone-supported-challenges` has been removed. ### Fixed * Certbot uses the Python library cryptography for OCSP when cryptography>=2.5 is installed. We fixed a bug in Certbot causing it to interpret timestamps in the OCSP response as being in the local timezone rather than UTC. * Issue causing the default CentOS 6 TLS configuration to ignore some of the HTTPS VirtualHosts created by Certbot. mod_ssl loading is now moved to main http.conf for this environment where possible. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * acme * certbot * certbot-apache * certbot-nginx More details about these changes can be found on our GitHub repo. ## 0.32.0 - 2019-03-06 ### Added * If possible, Certbot uses built-in support for OCSP from recent cryptography versions instead of the OpenSSL binary: as a consequence Certbot does not need the OpenSSL binary to be installed anymore if cryptography>=2.5 is installed. ### Changed * Certbot and its acme module now depend on josepy>=1.1.0 to avoid printing the warnings described at https://github.com/certbot/josepy/issues/13. * Apache plugin now respects CERTBOT_DOCS environment variable when adding command line defaults. * The running of manual plugin hooks is now always included in Certbot's log output. * Tests execution for certbot, certbot-apache and certbot-nginx packages now relies on pytest. * An ACME CA server may return a "Retry-After" HTTP header on authorization polling, as specified in the ACME protocol, to indicate when the next polling should occur. Certbot now reads this header if set and respect its value. * The `acme` module avoids sending the `keyAuthorization` field in the JWS payload when responding to a challenge as the field is not included in the current ACME protocol. To ease the migration path for ACME CA servers, Certbot and its `acme` module will first try the request without the `keyAuthorization` field but will temporarily retry the request with the field included if a `malformed` error is received. This fallback will be removed in version 0.34.0. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * acme * certbot * certbot-apache * certbot-nginx More details about these changes can be found on our GitHub repo. ## 0.31.0 - 2019-02-07 ### Added * Avoid reprocessing challenges that are already validated when a certificate is issued. * Support for initiating (but not solving end-to-end) TLS-ALPN-01 challenges with the `acme` module. ### Changed * Certbot's official Docker images are now based on Alpine Linux 3.9 rather than 3.7. The new version comes with OpenSSL 1.1.1. * Lexicon-based DNS plugins are now fully compatible with Lexicon 3.x (support on 2.x branch is maintained). * Apache plugin now attempts to configure all VirtualHosts matching requested domain name instead of only a single one when answering the HTTP-01 challenge. ### Fixed * Fixed accessing josepy contents through acme.jose when the full acme.jose path is used. * Clarify behavior for deleting certs as part of revocation. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * acme * certbot * certbot-apache * certbot-dns-cloudxns * certbot-dns-dnsimple * certbot-dns-dnsmadeeasy * certbot-dns-gehirn * certbot-dns-linode * certbot-dns-luadns * certbot-dns-nsone * certbot-dns-ovh * certbot-dns-sakuracloud More details about these changes can be found on our GitHub repo. ## 0.30.2 - 2019-01-25 ### Fixed * Update the version of setuptools pinned in certbot-auto to 40.6.3 to solve installation problems on newer OSes. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, this release only affects certbot-auto. More details about these changes can be found on our GitHub repo. ## 0.30.1 - 2019-01-24 ### Fixed * Always download the pinned version of pip in pipstrap to address breakages * Rename old,default.conf to old-and-default.conf to address commas in filenames breaking recent versions of pip. * Add VIRTUALENV_NO_DOWNLOAD=1 to all calls to virtualenv to address breakages from venv downloading the latest pip Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * certbot-apache More details about these changes can be found on our GitHub repo. ## 0.30.0 - 2019-01-02 ### Added * Added the `update_account` subcommand for account management commands. ### Changed * Copied account management functionality from the `register` subcommand to the `update_account` subcommand. * Marked usage `register --update-registration` for deprecation and removal in a future release. ### Fixed * Older modules in the josepy library can now be accessed through acme.jose like it could in previous versions of acme. This is only done to preserve backwards compatibility and support for doing this with new modules in josepy will not be added. Users of the acme library should switch to using josepy directly if they haven't done so already. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * acme More details about these changes can be found on our GitHub repo. ## 0.29.1 - 2018-12-05 ### Added * ### Changed * ### Fixed * The default work and log directories have been changed back to /var/lib/letsencrypt and /var/log/letsencrypt respectively. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * certbot More details about these changes can be found on our GitHub repo. ## 0.29.0 - 2018-12-05 ### Added * Noninteractive renewals with `certbot renew` (those not started from a terminal) now randomly sleep 1-480 seconds before beginning work in order to spread out load spikes on the server side. * Added External Account Binding support in cli and acme library. Command line arguments --eab-kid and --eab-hmac-key added. ### Changed * Private key permissioning changes: Renewal preserves existing group mode & gid of previous private key material. Private keys for new lineages (i.e. new certs, not renewed) default to 0o600. ### Fixed * Update code and dependencies to clean up Resource and Deprecation Warnings. * Only depend on imgconverter extension for Sphinx >= 1.6 Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * acme * certbot * certbot-apache * certbot-dns-cloudflare * certbot-dns-digitalocean * certbot-dns-google * certbot-nginx More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/62?closed=1 ## 0.28.0 - 2018-11-7 ### Added * `revoke` accepts `--cert-name`, and doesn't accept both `--cert-name` and `--cert-path`. * Use the ACMEv2 newNonce endpoint when a new nonce is needed, and newNonce is available in the directory. ### Changed * Removed documentation mentions of `#letsencrypt` IRC on Freenode. * Write README to the base of (config-dir)/live directory * `--manual` will explicitly warn users that earlier challenges should remain in place when setting up subsequent challenges. * Warn when using deprecated acme.challenges.TLSSNI01 * Log warning about TLS-SNI deprecation in Certbot * Stop preferring TLS-SNI in the Apache, Nginx, and standalone plugins * OVH DNS plugin now relies on Lexicon>=2.7.14 to support HTTP proxies * Default time the Linode plugin waits for DNS changes to propagate is now 1200 seconds. ### Fixed * Match Nginx parser update in allowing variable names to start with `${`. * Fix ranking of vhosts in Nginx so that all port-matching vhosts come first * Correct OVH integration tests on machines without internet access. * Stop caching the results of ipv6_info in http01.py * Test fix for Route53 plugin to prevent boto3 making outgoing connections. * The grammar used by Augeas parser in Apache plugin was updated to fix various parsing errors. * The CloudXNS, DNSimple, DNS Made Easy, Gehirn, Linode, LuaDNS, NS1, OVH, and Sakura Cloud DNS plugins are now compatible with Lexicon 3.0+. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * acme * certbot * certbot-apache * certbot-dns-cloudxns * certbot-dns-dnsimple * certbot-dns-dnsmadeeasy * certbot-dns-gehirn * certbot-dns-linode * certbot-dns-luadns * certbot-dns-nsone * certbot-dns-ovh * certbot-dns-route53 * certbot-dns-sakuracloud * certbot-nginx More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/59?closed=1 ## 0.27.1 - 2018-09-06 ### Fixed * Fixed parameter name in OpenSUSE overrides for default parameters in the Apache plugin. Certbot on OpenSUSE works again. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * certbot-apache More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/60?closed=1 ## 0.27.0 - 2018-09-05 ### Added * The Apache plugin now accepts the parameter --apache-ctl which can be used to configure the path to the Apache control script. ### Changed * When using `acme.client.ClientV2` (or `acme.client.BackwardsCompatibleClientV2` with an ACME server that supports a newer version of the ACME protocol), an `acme.errors.ConflictError` will be raised if you try to create an ACME account with a key that has already been used. Previously, a JSON parsing error was raised in this scenario when using the library with Let's Encrypt's ACMEv2 endpoint. ### Fixed * When Apache is not installed, Certbot's Apache plugin no longer prints messages about being unable to find apachectl to the terminal when the plugin is not selected. * If you're using the Apache plugin with the --apache-vhost-root flag set to a directory containing a disabled virtual host for the domain you're requesting a certificate for, the virtual host will now be temporarily enabled if necessary to pass the HTTP challenge. * The documentation for the Certbot package can now be built using Sphinx 1.6+. * You can now call `query_registration` without having to first call `new_account` on `acme.client.ClientV2` objects. * The requirement of `setuptools>=1.0` has been removed from `certbot-dns-ovh`. * Names in certbot-dns-sakuracloud's tests have been updated to refer to Sakura Cloud rather than NS1 whose plugin certbot-dns-sakuracloud was based on. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * acme * certbot * certbot-apache * certbot-dns-ovh * certbot-dns-sakuracloud More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/57?closed=1 ## 0.26.1 - 2018-07-17 ### Fixed * Fix a bug that was triggered when users who had previously manually set `--server` to get ACMEv2 certs tried to renew ACMEv1 certs. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: * certbot More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/58?closed=1 ## 0.26.0 - 2018-07-11 ### Added * A new security enhancement which we're calling AutoHSTS has been added to Certbot's Apache plugin. This enhancement configures your webserver to send a HTTP Strict Transport Security header with a low max-age value that is slowly increased over time. The max-age value is not increased to a large value until you've successfully managed to renew your certificate. This enhancement can be requested with the --auto-hsts flag. * New official DNS plugins have been created for Gehirn Infrastructure Service, Linode, OVH, and Sakura Cloud. These plugins can be found on our Docker Hub page at https://hub.docker.com/u/certbot and on PyPI. * The ability to reuse ACME accounts from Let's Encrypt's ACMEv1 endpoint on Let's Encrypt's ACMEv2 endpoint has been added. * Certbot and its components now support Python 3.7. * Certbot's install subcommand now allows you to interactively choose which certificate to install from the list of certificates managed by Certbot. * Certbot now accepts the flag `--no-autorenew` which causes any obtained certificates to not be automatically renewed when it approaches expiration. * Support for parsing the TLS-ALPN-01 challenge has been added back to the acme library. ### Changed * Certbot's default ACME server has been changed to Let's Encrypt's ACMEv2 endpoint. By default, this server will now be used for both new certificate lineages and renewals. * The Nginx plugin is no longer marked labeled as an "Alpha" version. * The `prepare` method of Certbot's plugins is no longer called before running "Updater" enhancements that are run on every invocation of `certbot renew`. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only packages with functional changes were: * acme * certbot * certbot-apache * certbot-dns-gehirn * certbot-dns-linode * certbot-dns-ovh * certbot-dns-sakuracloud * certbot-nginx More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/55?closed=1 ## 0.25.1 - 2018-06-13 ### Fixed * TLS-ALPN-01 support has been removed from our acme library. Using our current dependencies, we are unable to provide a correct implementation of this challenge so we decided to remove it from the library until we can provide proper support. * Issues causing test failures when running the tests in the acme package with pytest<3.0 has been resolved. * certbot-nginx now correctly depends on acme>=0.25.0. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only packages with changes other than their version number were: * acme * certbot-nginx More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/56?closed=1 ## 0.25.0 - 2018-06-06 ### Added * Support for the ready status type was added to acme. Without this change, Certbot and acme users will begin encountering errors when using Let's Encrypt's ACMEv2 API starting on June 19th for the staging environment and July 5th for production. See https://community.letsencrypt.org/t/acmev2-order-ready-status/62866 for more information. * Certbot now accepts the flag --reuse-key which will cause the same key to be used in the certificate when the lineage is renewed rather than generating a new key. * You can now add multiple email addresses to your ACME account with Certbot by providing a comma separated list of emails to the --email flag. * Support for Let's Encrypt's upcoming TLS-ALPN-01 challenge was added to acme. For more information, see https://community.letsencrypt.org/t/tls-alpn-validation-method/63814/1. * acme now supports specifying the source address to bind to when sending outgoing connections. You still cannot specify this address using Certbot. * If you run Certbot against Let's Encrypt's ACMEv2 staging server but don't already have an account registered at that server URL, Certbot will automatically reuse your staging account from Let's Encrypt's ACMEv1 endpoint if it exists. * Interfaces were added to Certbot allowing plugins to be called at additional points. The `GenericUpdater` interface allows plugins to perform actions every time `certbot renew` is run, regardless of whether any certificates are due for renewal, and the `RenewDeployer` interface allows plugins to perform actions when a certificate is renewed. See `certbot.interfaces` for more information. ### Changed * When running Certbot with --dry-run and you don't already have a staging account, the created account does not contain an email address even if one was provided to avoid expiration emails from Let's Encrypt's staging server. * certbot-nginx does a better job of automatically detecting the location of Nginx's configuration files when run on BSD based systems. * acme now requires and uses pytest when running tests with setuptools with `python setup.py test`. * `certbot config_changes` no longer waits for user input before exiting. ### Fixed * Misleading log output that caused users to think that Certbot's standalone plugin failed to bind to a port when performing a challenge has been corrected. * An issue where certbot-nginx would fail to enable HSTS if the server block already had an `add_header` directive has been resolved. * certbot-nginx now does a better job detecting the server block to base the configuration for TLS-SNI challenges on. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only packages with functional changes were: * acme * certbot * certbot-apache * certbot-nginx More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/54?closed=1 ## 0.24.0 - 2018-05-02 ### Added * certbot now has an enhance subcommand which allows you to configure security enhancements like HTTP to HTTPS redirects, OCSP stapling, and HSTS without reinstalling a certificate. * certbot-dns-rfc2136 now allows the user to specify the port to use to reach the DNS server in its credentials file. * acme now parses the wildcard field included in authorizations so it can be used by users of the library. ### Changed * certbot-dns-route53 used to wait for each DNS update to propagate before sending the next one, but now it sends all updates before waiting which speeds up issuance for multiple domains dramatically. * Certbot's official Docker images are now based on Alpine Linux 3.7 rather than 3.4 because 3.4 has reached its end-of-life. * We've doubled the time Certbot will spend polling authorizations before timing out. * The level of the message logged when Certbot is being used with non-standard paths warning that crontabs for renewal included in Certbot packages from OS package managers may not work has been reduced. This stops the message from being written to stderr every time `certbot renew` runs. ### Fixed * certbot-auto now works with Python 3.6. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only packages with changes other than their version number were: * acme * certbot * certbot-apache * certbot-dns-digitalocean (only style improvements to tests) * certbot-dns-rfc2136 More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/52?closed=1 ## 0.23.0 - 2018-04-04 ### Added * Support for OpenResty was added to the Nginx plugin. ### Changed * The timestamps in Certbot's logfiles now use the system's local time zone rather than UTC. * Certbot's DNS plugins that use Lexicon now rely on Lexicon>=2.2.1 to be able to create and delete multiple TXT records on a single domain. * certbot-dns-google's test suite now works without an internet connection. ### Fixed * Removed a small window that if during which an error occurred, Certbot wouldn't clean up performed challenges. * The parameters `default` and `ipv6only` are now removed from `listen` directives when creating a new server block in the Nginx plugin. * `server_name` directives enclosed in quotation marks in Nginx are now properly supported. * Resolved an issue preventing the Apache plugin from starting Apache when it's not currently running on RHEL and Gentoo based systems. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only packages with changes other than their version number were: * certbot * certbot-apache * certbot-dns-cloudxns * certbot-dns-dnsimple * certbot-dns-dnsmadeeasy * certbot-dns-google * certbot-dns-luadns * certbot-dns-nsone * certbot-dns-rfc2136 * certbot-nginx More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/50?closed=1 ## 0.22.2 - 2018-03-19 ### Fixed * A type error introduced in 0.22.1 that would occur during challenge cleanup when a Certbot plugin raises an exception while trying to complete the challenge was fixed. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only packages with changes other than their version number were: * certbot More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/53?closed=1 ## 0.22.1 - 2018-03-19 ### Changed * The ACME server used with Certbot's --dry-run and --staging flags is now Let's Encrypt's ACMEv2 staging server which allows people to also test ACMEv2 features with these flags. ### Fixed * The HTTP Content-Type header is now set to the correct value during certificate revocation with new versions of the ACME protocol. * When using Certbot with Let's Encrypt's ACMEv2 server, it would add a blank line to the top of chain.pem and between the certificates in fullchain.pem for each lineage. These blank lines have been removed. * Resolved a bug that caused Certbot's --allow-subset-of-names flag not to work. * Fixed a regression in acme.client.Client that caused the class to not work when it was initialized without a ClientNetwork which is done by some of the other projects using our ACME library. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only packages with changes other than their version number were: * acme * certbot More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/51?closed=1 ## 0.22.0 - 2018-03-07 ### Added * Support for obtaining wildcard certificates and a newer version of the ACME protocol such as the one implemented by Let's Encrypt's upcoming ACMEv2 endpoint was added to Certbot and its ACME library. Certbot still works with older ACME versions and will automatically change the version of the protocol used based on the version the ACME CA implements. * The Apache and Nginx plugins are now able to automatically install a wildcard certificate to multiple virtual hosts that you select from your server configuration. * The `certbot install` command now accepts the `--cert-name` flag for selecting a certificate. * `acme.client.BackwardsCompatibleClientV2` was added to Certbot's ACME library which automatically handles most of the differences between new and old ACME versions. `acme.client.ClientV2` is also available for people who only want to support one version of the protocol or want to handle the differences between versions themselves. * certbot-auto now supports the flag --install-only which has the script install Certbot and its dependencies and exit without invoking Certbot. * Support for issuing a single certificate for a wildcard and base domain was added to our Google Cloud DNS plugin. To do this, we now require your API credentials have additional permissions, however, your credentials will already have these permissions unless you defined a custom role with fewer permissions than the standard DNS administrator role provided by Google. These permissions are also only needed for the case described above so it will continue to work for existing users. For more information about the permissions changes, see the documentation in the plugin. ### Changed * We have broken lockstep between our ACME library, Certbot, and its plugins. This means that the different components do not need to be the same version to work together like they did previously. This makes packaging easier because not every piece of Certbot needs to be repackaged to ship a change to a subset of its components. * Support for Python 2.6 and Python 3.3 has been removed from ACME, Certbot, Certbot's plugins, and certbot-auto. If you are using certbot-auto on a RHEL 6 based system, it will walk you through the process of installing Certbot with Python 3 and refuse to upgrade to a newer version of Certbot until you have done so. * Certbot's components now work with older versions of setuptools to simplify packaging for EPEL 7. ### Fixed * Issues caused by Certbot's Nginx plugin adding multiple ipv6only directives has been resolved. * A problem where Certbot's Apache plugin would add redundant include directives for the TLS configuration managed by Certbot has been fixed. * Certbot's webroot plugin now properly deletes any directories it creates. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/48?closed=1 ## 0.21.1 - 2018-01-25 ### Fixed * When creating an HTTP to HTTPS redirect in Nginx, we now ensure the Host header of the request is set to an expected value before redirecting users to the domain found in the header. The previous way Certbot configured Nginx redirects was a potential security issue which you can read more about at https://community.letsencrypt.org/t/security-issue-with-redirects-added-by-certbots-nginx-plugin/51493. * Fixed a problem where Certbot's Apache plugin could fail HTTP-01 challenges if basic authentication is configured for the domain you request a certificate for. * certbot-auto --no-bootstrap now properly tries to use Python 3.4 on RHEL 6 based systems rather than Python 2.6. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/49?closed=1 ## 0.21.0 - 2018-01-17 ### Added * Support for the HTTP-01 challenge type was added to our Apache and Nginx plugins. For those not aware, Let's Encrypt disabled the TLS-SNI-01 challenge type which was what was previously being used by our Apache and Nginx plugins last week due to a security issue. For more information about Let's Encrypt's change, click [here](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188). Our Apache and Nginx plugins will automatically switch to use HTTP-01 so no changes need to be made to your Certbot configuration, however, you should make sure your server is accessible on port 80 and isn't behind an external proxy doing things like redirecting all traffic from HTTP to HTTPS. HTTP to HTTPS redirects inside Apache and Nginx are fine. * IPv6 support was added to the Nginx plugin. * Support for automatically creating server blocks based on the default server block was added to the Nginx plugin. * The flags --delete-after-revoke and --no-delete-after-revoke were added allowing users to control whether the revoke subcommand also deletes the certificates it is revoking. ### Changed * We deprecated support for Python 2.6 and Python 3.3 in Certbot and its ACME library. Support for these versions of Python will be removed in the next major release of Certbot. If you are using certbot-auto on a RHEL 6 based system, it will guide you through the process of installing Python 3. * We split our implementation of JOSE (Javascript Object Signing and Encryption) out of our ACME library and into a separate package named josepy. This package is available on [PyPI](https://pypi.python.org/pypi/josepy) and on [GitHub](https://github.com/certbot/josepy). * We updated the ciphersuites used in Apache to the new [values recommended by Mozilla](https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29). The major change here is adding ChaCha20 to the list of supported ciphersuites. ### Fixed * An issue with our Apache plugin on Gentoo due to differences in their apache2ctl command have been resolved. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/47?closed=1 ## 0.20.0 - 2017-12-06 ### Added * Certbot's ACME library now recognizes URL fields in challenge objects in preparation for Let's Encrypt's new ACME endpoint. The value is still accessible in our ACME library through the name "uri". ### Changed * The Apache plugin now parses some distro specific Apache configuration files on non-Debian systems allowing it to get a clearer picture on the running configuration. Internally, these changes were structured so that external contributors can easily write patches to make the plugin work in new Apache configurations. * Certbot better reports network failures by removing information about connection retries from the error output. * An unnecessary question when using Certbot's webroot plugin interactively has been removed. ### Fixed * Certbot's NGINX plugin no longer sometimes incorrectly reports that it was unable to deploy a HTTP->HTTPS redirect when requesting Certbot to enable a redirect for multiple domains. * Problems where the Apache plugin was failing to find directives and duplicating existing directives on openSUSE have been resolved. * An issue running the test shipped with Certbot and some our DNS plugins with older versions of mock have been resolved. * On some systems, users reported strangely interleaved output depending on when stdout and stderr were flushed. This problem was resolved by having Certbot regularly flush these streams. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/44?closed=1 ## 0.19.0 - 2017-10-04 ### Added * Certbot now has renewal hook directories where executable files can be placed for Certbot to run with the renew subcommand. Pre-hooks, deploy-hooks, and post-hooks can be specified in the renewal-hooks/pre, renewal-hooks/deploy, and renewal-hooks/post directories respectively in Certbot's configuration directory (which is /etc/letsencrypt by default). Certbot will automatically create these directories when it is run if they do not already exist. * After revoking a certificate with the revoke subcommand, Certbot will offer to delete the lineage associated with the certificate. When Certbot is run with --non-interactive, it will automatically try to delete the associated lineage. * When using Certbot's Google Cloud DNS plugin on Google Compute Engine, you no longer have to provide a credential file to Certbot if you have configured sufficient permissions for the instance which Certbot can automatically obtain using Google's metadata service. ### Changed * When deleting certificates interactively using the delete subcommand, Certbot will now allow you to select multiple lineages to be deleted at once. * Certbot's Apache plugin no longer always parses Apache's sites-available on Debian based systems and instead only parses virtual hosts included in your Apache configuration. You can provide an additional directory for Certbot to parse using the command line flag --apache-vhost-root. ### Fixed * The plugins subcommand can now be run without root access. * certbot-auto now includes a timeout when updating itself so it no longer hangs indefinitely when it is unable to connect to the external server. * An issue where Certbot's Apache plugin would sometimes fail to deploy a certificate on Debian based systems if mod_ssl wasn't already enabled has been resolved. * A bug in our Docker image where the certificates subcommand could not report if certificates maintained by Certbot had been revoked has been fixed. * Certbot's RFC 2136 DNS plugin (for use with software like BIND) now properly performs DNS challenges when the domain being verified contains a CNAME record. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/43?closed=1 ## 0.18.2 - 2017-09-20 ### Fixed * An issue where Certbot's ACME module would raise an AttributeError trying to create self-signed certificates when used with pyOpenSSL 17.3.0 has been resolved. For Certbot users with this version of pyOpenSSL, this caused Certbot to crash when performing a TLS SNI challenge or when the Nginx plugin tried to create an SSL server block. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/46?closed=1 ## 0.18.1 - 2017-09-08 ### Fixed * If certbot-auto was running as an unprivileged user and it upgraded from 0.17.0 to 0.18.0, it would crash with a permissions error and would need to be run again to successfully complete the upgrade. This has been fixed and certbot-auto should upgrade cleanly to 0.18.1. * Certbot usually uses "certbot-auto" or "letsencrypt-auto" in error messages and the User-Agent string instead of "certbot" when you are using one of these wrapper scripts. Proper detection of this was broken with Certbot's new installation path in /opt in 0.18.0 but this problem has been resolved. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/45?closed=1 ## 0.18.0 - 2017-09-06 ### Added * The Nginx plugin now configures Nginx to use 2048-bit Diffie-Hellman parameters. Java 6 clients do not support Diffie-Hellman parameters larger than 1024 bits, so if you need to support these clients you will need to manually modify your Nginx configuration after using the Nginx installer. ### Changed * certbot-auto now installs Certbot in directories under `/opt/eff.org`. If you had an existing installation from certbot-auto, a symlink is created to the new directory. You can configure certbot-auto to use a different path by setting the environment variable VENV_PATH. * The Nginx plugin can now be selected in Certbot's interactive output. * Output verbosity of renewal failures when running with `--quiet` has been reduced. * The default revocation reason shown in Certbot help output now is a human readable string instead of a numerical code. * Plugin selection is now included in normal terminal output. ### Fixed * A newer version of ConfigArgParse is now installed when using certbot-auto causing values set to false in a Certbot INI configuration file to be handled intuitively. Setting a boolean command line flag to false is equivalent to not including it in the configuration file at all. * New naming conventions preventing certbot-auto from installing OS dependencies on Fedora 26 have been resolved. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/42?closed=1 ## 0.17.0 - 2017-08-02 ### Added * Support in our nginx plugin for modifying SSL server blocks that do not contain certificate or key directives. * A `--max-log-backups` flag to allow users to configure or even completely disable Certbot's built in log rotation. * A `--user-agent-comment` flag to allow people who build tools around Certbot to differentiate their user agent string by adding a comment to its default value. ### Changed * Due to some awesome work by [cryptography project](https://github.com/pyca/cryptography), compilation can now be avoided on most systems when using certbot-auto. This eliminates many problems people have had in the past such as running out of memory, having invalid headers/libraries, and changes to the OS packages on their system after compilation breaking Certbot. * The `--renew-hook` flag has been hidden in favor of `--deploy-hook`. This new flag works exactly the same way except it is always run when a certificate is issued rather than just when it is renewed. * We have started printing deprecation warnings in certbot-auto for experimentally supported systems with OS packages available. * A certificate lineage's name is included in error messages during renewal. ### Fixed * Encoding errors that could occur when parsing error messages from the ACME server containing Unicode have been resolved. * certbot-auto no longer prints misleading messages about there being a newer pip version available when installation fails. * Certbot's ACME library now properly extracts domains from critical SAN extensions. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.17.0+is%3Aclosed ## 0.16.0 - 2017-07-05 ### Added * A plugin for performing DNS challenges using dynamic DNS updates as defined in RFC 2316. This plugin is packaged separately from Certbot and is available at https://pypi.python.org/pypi/certbot-dns-rfc2136. It supports Python 2.6, 2.7, and 3.3+. At this time, there isn't a good way to install this plugin when using certbot-auto, but this should change in the near future. * Plugins for performing DNS challenges for the providers [DNS Made Easy](https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy) and [LuaDNS](https://pypi.python.org/pypi/certbot-dns-luadns). These plugins are packaged separately from Certbot and support Python 2.7 and 3.3+. Currently, there isn't a good way to install these plugins when using certbot-auto, but that should change soon. * Support for performing TLS-SNI-01 challenges when using the manual plugin. * Automatic detection of Arch Linux in the Apache plugin providing better default settings for the plugin. ### Changed * The text of the interactive question about whether a redirect from HTTP to HTTPS should be added by Certbot has been rewritten to better explain the choices to the user. * Simplified HTTP challenge instructions in the manual plugin. ### Fixed * Problems performing a dry run when using the Nginx plugin have been fixed. * Resolved an issue where certbot-dns-digitalocean's test suite would sometimes fail when ran using Python 3. * On some systems, previous versions of certbot-auto would error out with a message about a missing hash for setuptools. This has been fixed. * A bug where Certbot would sometimes not print a space at the end of an interactive prompt has been resolved. * Nonfatal tracebacks are no longer shown in rare cases where Certbot encounters an exception trying to close its TCP connection with the ACME server. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.16.0+is%3Aclosed ## 0.15.0 - 2017-06-08 ### Added * Plugins for performing DNS challenges for popular providers. Like the Apache and Nginx plugins, these plugins are packaged separately and not included in Certbot by default. So far, we have plugins for [Amazon Route 53](https://pypi.python.org/pypi/certbot-dns-route53), [Cloudflare](https://pypi.python.org/pypi/certbot-dns-cloudflare), [DigitalOcean](https://pypi.python.org/pypi/certbot-dns-digitalocean), and [Google Cloud](https://pypi.python.org/pypi/certbot-dns-google) which all work on Python 2.6, 2.7, and 3.3+. Additionally, we have plugins for [CloudXNS](https://pypi.python.org/pypi/certbot-dns-cloudxns), [DNSimple](https://pypi.python.org/pypi/certbot-dns-dnsimple), [NS1](https://pypi.python.org/pypi/certbot-dns-nsone) which work on Python 2.7 and 3.3+ (and not 2.6). Currently, there isn't a good way to install these plugins when using `certbot-auto`, but that should change soon. * IPv6 support in the standalone plugin. When performing a challenge, the standalone plugin automatically handles listening for IPv4/IPv6 traffic based on the configuration of your system. * A mechanism for keeping your Apache and Nginx SSL/TLS configuration up to date. When the Apache or Nginx plugins are used, they place SSL/TLS configuration options in the root of Certbot's config directory (`/etc/letsencrypt` by default). Now when a new version of these plugins run on your system, they will automatically update the file to the newest version if it is unmodified. If you manually modified the file, Certbot will display a warning giving you a path to the updated file which you can use as a reference to manually update your modified copy. * `--http-01-address` and `--tls-sni-01-address` flags for controlling the address Certbot listens on when using the standalone plugin. * The command `certbot certificates` that lists certificates managed by Certbot now performs additional validity checks to notify you if your files have become corrupted. ### Changed * Messages custom hooks print to `stdout` are now displayed by Certbot when not running in `--quiet` mode. * `jwk` and `alg` fields in JWS objects have been moved into the protected header causing Certbot to more closely follow the latest version of the ACME spec. ### Fixed * Permissions on renewal configuration files are now properly preserved when they are updated. * A bug causing Certbot to display strange defaults in its help output when using Python <= 2.7.4 has been fixed. * Certbot now properly handles mixed case domain names found in custom CSRs. * A number of poorly worded prompts and error messages. ### Removed * Support for OpenSSL 1.0.0 in `certbot-auto` has been removed as we now pin a newer version of `cryptography` which dropped support for this version. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.15.0+is%3Aclosed ## 0.14.2 - 2017-05-25 ### Fixed * Certbot 0.14.0 included a bug where Certbot would create a temporary log file (usually in /tmp) if the program exited during argument parsing. If a user provided -h/--help/help, --version, or an invalid command line argument, Certbot would create this temporary log file. This was especially bothersome to certbot-auto users as certbot-auto runs `certbot --version` internally to see if the script needs to upgrade causing it to create at least one of these files on every run. This problem has been resolved. More details about this change can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.14.2+is%3Aclosed ## 0.14.1 - 2017-05-16 ### Fixed * Certbot now works with configargparse 0.12.0. * Issues with the Apache plugin and Augeas 1.7+ have been resolved. * A problem where the Nginx plugin would fail to install certificates on systems that had the plugin's SSL/TLS options file from 7+ months ago has been fixed. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.14.1+is%3Aclosed ## 0.14.0 - 2017-05-04 ### Added * Python 3.3+ support for all Certbot packages. `certbot-auto` still currently only supports Python 2, but the `acme`, `certbot`, `certbot-apache`, and `certbot-nginx` packages on PyPI now fully support Python 2.6, 2.7, and 3.3+. * Certbot's Apache plugin now handles multiple virtual hosts per file. * Lockfiles to prevent multiple versions of Certbot running simultaneously. ### Changed * When converting an HTTP virtual host to HTTPS in Apache, Certbot only copies the virtual host rather than the entire contents of the file it's contained in. * The Nginx plugin now includes SSL/TLS directives in a separate file located in Certbot's configuration directory rather than copying the contents of the file into every modified `server` block. ### Fixed * Ensure logging is configured before parts of Certbot attempt to log any messages. * Support for the `--quiet` flag in `certbot-auto`. * Reverted a change made in a previous release to make the `acme` and `certbot` packages always depend on `argparse`. This dependency is conditional again on the user's Python version. * Small bugs in the Nginx plugin such as properly handling empty `server` blocks and setting `server_names_hash_bucket_size` during challenges. As always, a more complete list of changes can be found on GitHub: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.14.0+is%3Aclosed ## 0.13.0 - 2017-04-06 ### Added * `--debug-challenges` now pauses Certbot after setting up challenges for debugging. * The Nginx parser can now handle all valid directives in configuration files. * Nginx ciphersuites have changed to Mozilla Intermediate. * `certbot-auto --no-bootstrap` provides the option to not install OS dependencies. ### Fixed * `--register-unsafely-without-email` now respects `--quiet`. * Hyphenated renewal parameters are now saved in renewal config files. * `--dry-run` no longer persists keys and csrs. * Certbot no longer hangs when trying to start Nginx in Arch Linux. * Apache rewrite rules no longer double-encode characters. A full list of changes is available on GitHub: https://github.com/certbot/certbot/issues?q=is%3Aissue%20milestone%3A0.13.0%20is%3Aclosed%20 ## 0.12.0 - 2017-03-02 ### Added * Certbot now allows non-camelcase Apache VirtualHost names. * Certbot now allows more log messages to be silenced. ### Fixed * Fixed a regression around using `--cert-name` when getting new certificates More information about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue%20milestone%3A0.12.0 ## 0.11.1 - 2017-02-01 ### Fixed * Resolved a problem where Certbot would crash while parsing command line arguments in some cases. * Fixed a typo. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/pulls?q=is%3Apr%20milestone%3A0.11.1%20is%3Aclosed ## 0.11.0 - 2017-02-01 ### Added * When using the standalone plugin while running Certbot interactively and a required port is bound by another process, Certbot will give you the option to retry to grab the port rather than immediately exiting. * You are now able to deactivate your account with the Let's Encrypt server using the `unregister` subcommand. * When revoking a certificate using the `revoke` subcommand, you now have the option to provide the reason the certificate is being revoked to Let's Encrypt with `--reason`. ### Changed * Providing `--quiet` to `certbot-auto` now silences package manager output. ### Removed * Removed the optional `dnspython` dependency in our `acme` package. Now the library does not support client side verification of the DNS challenge. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.11.0+is%3Aclosed ## 0.10.2 - 2017-01-25 ### Added * If Certbot receives a request with a `badNonce` error, it now automatically retries the request. Since nonces from Let's Encrypt expire, this helps people performing the DNS challenge with the `manual` plugin who may have to wait an extended period of time for their DNS changes to propagate. ### Fixed * Certbot now saves the `--preferred-challenges` values for renewal. Previously these values were discarded causing a different challenge type to be used when renewing certs in some cases. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.10.2+is%3Aclosed ## 0.10.1 - 2017-01-13 ### Fixed * Resolve problems where when asking Certbot to update a certificate at an existing path to include different domain names, the old names would continue to be used. * Fix issues successfully running our unit test suite on some systems. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.10.1+is%3Aclosed ## 0.10.0 - 2017-01-11 ## Added * Added the ability to customize and automatically complete DNS and HTTP domain validation challenges with the manual plugin. The flags `--manual-auth-hook` and `--manual-cleanup-hook` can now be provided when using the manual plugin to execute commands provided by the user to perform and clean up challenges provided by the CA. This is best used in complicated setups where the DNS challenge must be used or Certbot's existing plugins cannot be used to perform HTTP challenges. For more information on how this works, see `certbot --help manual`. * Added a `--cert-name` flag for specifying the name to use for the certificate in Certbot's configuration directory. Using this flag in combination with `-d/--domains`, a user can easily request a new certificate with different domains and save it with the name provided by `--cert-name`. Additionally, `--cert-name` can be used to select a certificate with the `certonly` and `run` subcommands so a full list of domains in the certificate does not have to be provided. * Added subcommand `certificates` for listing the certificates managed by Certbot and their properties. * Added the `delete` subcommand for removing certificates managed by Certbot from the configuration directory. * Certbot now supports requesting internationalized domain names (IDNs). * Hooks provided to Certbot are now saved to be reused during renewal. If you run Certbot with `--pre-hook`, `--renew-hook`, or `--post-hook` flags when obtaining a certificate, the provided commands will automatically be saved and executed again when renewing the certificate. A pre-hook and/or post-hook can also be given to the `certbot renew` command either on the command line or in a [configuration file](https://certbot.eff.org/docs/using.html#configuration-file) to run an additional command before/after any certificate is renewed. Hooks will only be run if a certificate is renewed. * Support Busybox in certbot-auto. ### Changed * Recategorized `-h/--help` output to improve documentation and discoverability. ### Removed * Removed the ncurses interface. This change solves problems people were having on many systems, reduces the number of Certbot dependencies, and simplifies our code. Certbot's only interface now is the text interface which was available by providing `-t/--text` to earlier versions of Certbot. ### Fixed * Many small bug fixes. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.10.0is%3Aclosed ## 0.9.3 - 2016-10-13 ### Added * The Apache plugin uses information about your OS to help determine the layout of your Apache configuration directory. We added a patch to ensure this code behaves the same way when testing on different systems as the tests were failing in some cases. ### Changed * Certbot adopted more conservative behavior about reporting a needed port as unavailable when using the standalone plugin. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/27?closed=1 ## 0.9.2 - 2016-10-12 ### Added * Certbot stopped requiring that all possibly required ports are available when using the standalone plugin. It now only verifies that the ports are available when they are necessary. ### Fixed * Certbot now verifies that our optional dependencies version matches what is required by Certbot. * Certnot now properly copies the `ssl on;` directives as necessary when performing domain validation in the Nginx plugin. * Fixed problem where symlinks were becoming files when they were packaged, causing errors during testing and OS packaging. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/26?closed=1 ## 0.9.1 - 2016-10-06 ### Fixed * Fixed a bug that was introduced in version 0.9.0 where the command line flag -q/--quiet wasn't respected in some cases. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/25?closed=1 ## 0.9.0 - 2016-10-05 ### Added * Added an alpha version of the Nginx plugin. This plugin fully automates the process of obtaining and installing certificates with Nginx. Additionally, it is able to automatically configure security enhancements such as an HTTP to HTTPS redirect and OCSP stapling. To use this plugin, you must have the `certbot-nginx` package installed (which is installed automatically when using `certbot-auto`) and provide `--nginx` on the command line. This plugin is still in its early stages so we recommend you use it with some caution and make sure you have a backup of your Nginx configuration. * Added support for the `DNS` challenge in the `acme` library and `DNS` in Certbot's `manual` plugin. This allows you to create DNS records to prove to Let's Encrypt you control the requested domain name. To use this feature, include `--manual --preferred-challenges dns` on the command line. * Certbot now helps with enabling Extra Packages for Enterprise Linux (EPEL) on CentOS 6 when using `certbot-auto`. To use `certbot-auto` on CentOS 6, the EPEL repository has to be enabled. `certbot-auto` will now prompt users asking them if they would like the script to enable this for them automatically. This is done without prompting users when using `letsencrypt-auto` or if `-n/--non-interactive/--noninteractive` is included on the command line. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.9.0+is%3Aclosed ## 0.8.1 - 2016-06-14 ### Added * Certbot now preserves a certificate's common name when using `renew`. * Certbot now saves webroot values for renewal when they are entered interactively. * Certbot now gracefully reports that the Apache plugin isn't usable when Augeas is not installed. * Added experimental support for Mageia has been added to `certbot-auto`. ### Fixed * Fixed problems with an invalid user-agent string on OS X. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.8.1+ ## 0.8.0 - 2016-06-02 ### Added * Added the `register` subcommand which can be used to register an account with the Let's Encrypt CA. * You can now run `certbot register --update-registration` to change the e-mail address associated with your registration. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.8.0+ ## 0.7.0 - 2016-05-27 ### Added * Added `--must-staple` to request certificates from Let's Encrypt with the OCSP must staple extension. * Certbot now automatically configures OSCP stapling for Apache. * Certbot now allows requesting certificates for domains found in the common name of a custom CSR. ### Fixed * Fixed a number of miscellaneous bugs More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=milestone%3A0.7.0+is%3Aissue ## 0.6.0 - 2016-05-12 ### Added * Versioned the datetime dependency in setup.py. ### Changed * Renamed the client from `letsencrypt` to `certbot`. ### Fixed * Fixed a small json deserialization error. * Certbot now preserves domain order in generated CSRs. * Fixed some minor bugs. More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/issues?q=is%3Aissue%20milestone%3A0.6.0%20is%3Aclosed%20 ## 0.5.0 - 2016-04-05 ### Added * Added the ability to use the webroot plugin interactively. * Added the flags --pre-hook, --post-hook, and --renew-hook which can be used with the renew subcommand to register shell commands to run in response to renewal events. Pre-hook commands will be run before any certs are renewed, post-hook commands will be run after any certs are renewed, and renew-hook commands will be run after each cert is renewed. If no certs are due for renewal, no command is run. * Added a -q/--quiet flag which silences all output except errors. * Added an --allow-subset-of-domains flag which can be used with the renew command to prevent renewal failures for a subset of the requested domains from causing the client to exit. ### Changed * Certbot now uses renewal configuration files. In /etc/letsencrypt/renewal by default, these files can be used to control what parameters are used when renewing a specific certificate. More details about these changes can be found on our GitHub repo: https://github.com/letsencrypt/letsencrypt/issues?q=milestone%3A0.5.0+is%3Aissue ## 0.4.2 - 2016-03-03 ### Fixed * Resolved problems encountered when compiling letsencrypt against the new OpenSSL release. * Fixed problems encountered when using `letsencrypt renew` with configuration files from the private beta. More details about these changes can be found on our GitHub repo: https://github.com/letsencrypt/letsencrypt/issues?q=is%3Aissue+milestone%3A0.4.2 ## 0.4.1 - 2016-02-29 ### Fixed * Fixed Apache parsing errors encountered with some configurations. * Fixed Werkzeug dependency problems encountered on some Red Hat systems. * Fixed bootstrapping failures when using letsencrypt-auto with --no-self-upgrade. * Fixed problems with parsing renewal config files from private beta. More details about these changes can be found on our GitHub repo: https://github.com/letsencrypt/letsencrypt/issues?q=is:issue+milestone:0.4.1 ## 0.4.0 - 2016-02-10 ### Added * Added the verb/subcommand `renew` which can be used to renew your existing certificates as they approach expiration. Running `letsencrypt renew` will examine all existing certificate lineages and determine if any are less than 30 days from expiration. If so, the client will use the settings provided when you previously obtained the certificate to renew it. The subcommand finishes by printing a summary of which renewals were successful, failed, or not yet due. * Added a `--dry-run` flag to help with testing configuration without affecting production rate limits. Currently supported by the `renew` and `certonly` subcommands, providing `--dry-run` on the command line will obtain certificates from the staging server without saving the resulting certificates to disk. * Added major improvements to letsencrypt-auto. This script has been rewritten to include full support for Python 2.6, the ability for letsencrypt-auto to update itself, and improvements to the stability, security, and performance of the script. * Added support for Apache 2.2 to the Apache plugin. More details about these changes can be found on our GitHub repo: https://github.com/letsencrypt/letsencrypt/issues?q=is%3Aissue+milestone%3A0.4.0 ## 0.3.0 - 2016-01-27 ### Added * Added a non-interactive mode which can be enabled by including `-n` or `--non-interactive` on the command line. This can be used to guarantee the client will not prompt when run automatically using cron/systemd. * Added preparation for the new letsencrypt-auto script. Over the past couple months, we've been working on increasing the reliability and security of letsencrypt-auto. A number of changes landed in this release to prepare for the new version of this script. More details about these changes can be found on our GitHub repo: https://github.com/letsencrypt/letsencrypt/issues?q=is%3Aissue+milestone%3A0.3.0 ## 0.2.0 - 2016-01-14 ### Added * Added Apache plugin support for non-Debian based systems. Support has been added for modern Red Hat based systems such as Fedora 23, Red Hat 7, and CentOS 7 running Apache 2.4. In theory, this plugin should be able to be configured to run on any Unix-like OS running Apache 2.4. * Relaxed PyOpenSSL version requirements. This adds support for systems with PyOpenSSL versions 0.13 or 0.14. * Improved error messages from the client. ### Fixed * Resolved issues with the Apache plugin enabling an HTTP to HTTPS redirect on some systems. More details about these changes can be found on our GitHub repo: https://github.com/letsencrypt/letsencrypt/issues?q=is%3Aissue+milestone%3A0.2.0 ## 0.1.1 - 2015-12-15 ### Added * Added a check that avoids attempting to issue for unqualified domain names like "localhost". ### Fixed * Fixed a confusing UI path that caused some users to repeatedly renew their certs while experimenting with the client, in some cases hitting issuance rate limits. * Fixed numerous Apache configuration parser problems * Fixed --webroot permission handling for non-root users More details about these changes can be found on our GitHub repo: https://github.com/letsencrypt/letsencrypt/issues?q=milestone%3A0.1.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/LICENSE.txt0000664000175100017510000002630014561227515014014 0ustar00ericaericaCertbot ACME Client Copyright (c) Electronic Frontier Foundation and others Licensed Apache Version 2.0 The nginx plugin incorporates code from nginxparser Copyright (c) 2014 Fatih Erikli Licensed MIT Text of Apache License ====================== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. Text of MIT License =================== Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/MANIFEST.in0000664000175100017510000000041314561227515013724 0ustar00ericaericainclude README.rst include CHANGELOG.md include LICENSE.txt recursive-include docs * recursive-include examples * recursive-include certbot/tests/testdata * include certbot/ssl-dhparams.pem include certbot/py.typed global-exclude __pycache__ global-exclude *.py[cod] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3470836 certbot-2.9.0/PKG-INFO0000644000175100017510000001770214561227516013273 0ustar00ericaericaMetadata-Version: 2.1 Name: certbot Version: 2.9.0 Summary: ACME client Home-page: https://github.com/certbot/certbot Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities Requires-Python: >=3.8 License-File: LICENSE.txt Requires-Dist: acme>=2.9.0 Requires-Dist: ConfigArgParse>=1.5.3 Requires-Dist: configobj>=5.0.6 Requires-Dist: cryptography>=3.2.1 Requires-Dist: distro>=1.0.1 Requires-Dist: importlib_resources>=1.3.1; python_version < "3.9" Requires-Dist: importlib_metadata>=4.6; python_version < "3.10" Requires-Dist: josepy>=1.13.0 Requires-Dist: parsedatetime>=2.4 Requires-Dist: pyrfc3339 Requires-Dist: pytz>=2019.3 Requires-Dist: pywin32>=300; sys_platform == "win32" Requires-Dist: setuptools>=41.6.0 Provides-Extra: all Requires-Dist: azure-devops; extra == "all" Requires-Dist: ipdb; extra == "all" Requires-Dist: poetry>=1.2.0; extra == "all" Requires-Dist: poetry-plugin-export>=1.1.0; extra == "all" Requires-Dist: twine; extra == "all" Requires-Dist: Sphinx>=1.2; extra == "all" Requires-Dist: sphinx_rtd_theme; extra == "all" Requires-Dist: coverage; extra == "all" Requires-Dist: mypy; extra == "all" Requires-Dist: pip; extra == "all" Requires-Dist: pylint; extra == "all" Requires-Dist: pytest; extra == "all" Requires-Dist: pytest-cov; extra == "all" Requires-Dist: pytest-xdist; extra == "all" Requires-Dist: setuptools; extra == "all" Requires-Dist: tox; extra == "all" Requires-Dist: types-httplib2; extra == "all" Requires-Dist: types-pyOpenSSL; extra == "all" Requires-Dist: types-pyRFC3339; extra == "all" Requires-Dist: types-pytz; extra == "all" Requires-Dist: types-pywin32; extra == "all" Requires-Dist: types-requests; extra == "all" Requires-Dist: types-setuptools; extra == "all" Requires-Dist: types-six; extra == "all" Requires-Dist: wheel; extra == "all" Provides-Extra: dev Requires-Dist: azure-devops; extra == "dev" Requires-Dist: ipdb; extra == "dev" Requires-Dist: poetry>=1.2.0; extra == "dev" Requires-Dist: poetry-plugin-export>=1.1.0; extra == "dev" Requires-Dist: twine; extra == "dev" Provides-Extra: docs Requires-Dist: Sphinx>=1.2; extra == "docs" Requires-Dist: sphinx_rtd_theme; extra == "docs" Provides-Extra: test Requires-Dist: coverage; extra == "test" Requires-Dist: mypy; extra == "test" Requires-Dist: pip; extra == "test" Requires-Dist: pylint; extra == "test" Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: pytest-xdist; extra == "test" Requires-Dist: setuptools; extra == "test" Requires-Dist: tox; extra == "test" Requires-Dist: types-httplib2; extra == "test" Requires-Dist: types-pyOpenSSL; extra == "test" Requires-Dist: types-pyRFC3339; extra == "test" Requires-Dist: types-pytz; extra == "test" Requires-Dist: types-pywin32; extra == "test" Requires-Dist: types-requests; extra == "test" Requires-Dist: types-setuptools; extra == "test" Requires-Dist: types-six; extra == "test" Requires-Dist: wheel; extra == "test" .. This file contains a series of comments that are used to include sections of this README in other files. Do not modify these comments unless you know what you are doing. tag:intro-begin |build-status| .. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/master :target: https://dev.azure.com/certbot/certbot/_build?definitionId=5 :alt: Azure Pipelines CI status .. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/eff-certbot-lockup.png :width: 200 :alt: EFF Certbot Logo Certbot is part of EFF’s effort to encrypt the entire Internet. Secure communication over the Web relies on HTTPS, which requires the use of a digital certificate that lets browsers verify the identity of web servers (e.g., is that really google.com?). Web servers obtain their certificates from trusted third parties called certificate authorities (CAs). Certbot is an easy-to-use client that fetches a certificate from Let’s Encrypt—an open certificate authority launched by the EFF, Mozilla, and others—and deploys it to a web server. Anyone who has gone through the trouble of setting up a secure website knows what a hassle getting and maintaining a certificate is. Certbot and Let’s Encrypt can automate away the pain and let you turn on and manage HTTPS with simple commands. Using Certbot and Let's Encrypt is free. .. _installation: Getting Started --------------- The best way to get started is to use our `interactive guide `_. It generates instructions based on your configuration settings. In most cases, you’ll need `root or administrator access `_ to your web server to run Certbot. Certbot is meant to be run directly on your web server on the command line, not on your personal computer. If you’re using a hosted service and don’t have direct access to your web server, you might not be able to use Certbot. Check with your hosting provider for documentation about uploading certificates or using certificates issued by Let’s Encrypt. Contributing ------------ If you'd like to contribute to this project please read `Developer Guide `_. This project is governed by `EFF's Public Projects Code of Conduct `_. Links ===== .. Do not modify this comment unless you know what you're doing. tag:links-begin Documentation: https://certbot.eff.org/docs Software project: https://github.com/certbot/certbot Changelog: https://github.com/certbot/certbot/blob/master/certbot/CHANGELOG.md For Contributors: https://certbot.eff.org/docs/contributing.html For Users: https://certbot.eff.org/docs/using.html Main Website: https://certbot.eff.org Let's Encrypt Website: https://letsencrypt.org Community: https://community.letsencrypt.org ACME spec: `RFC 8555 `_ ACME working area in github (archived): https://github.com/ietf-wg-acme/acme .. Do not modify this comment unless you know what you're doing. tag:links-end .. Do not modify this comment unless you know what you're doing. tag:intro-end .. Do not modify this comment unless you know what you're doing. tag:features-begin Current Features ===================== * Supports multiple web servers: - Apache 2.4+ - nginx/0.8.48+ - webroot (adds files to webroot directories in order to prove control of domains and obtain certificates) - standalone (runs its own simple webserver to prove you control a domain) - other server software via `third party plugins `_ * The private key is generated locally on your system. * Can talk to the Let's Encrypt CA or optionally to other ACME compliant services. * Can get domain-validated (DV) certificates. * Can revoke certificates. * Supports ECDSA (default) and RSA certificate private keys. * Can optionally install a http -> https redirect, so your site effectively runs https only. * Fully automated. * Configuration changes are logged and can be reverted. .. Do not modify this comment unless you know what you're doing. tag:features-end ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/README.rst0000664000175100017510000001025014561227515013655 0ustar00ericaerica.. This file contains a series of comments that are used to include sections of this README in other files. Do not modify these comments unless you know what you are doing. tag:intro-begin |build-status| .. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/master :target: https://dev.azure.com/certbot/certbot/_build?definitionId=5 :alt: Azure Pipelines CI status .. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/eff-certbot-lockup.png :width: 200 :alt: EFF Certbot Logo Certbot is part of EFF’s effort to encrypt the entire Internet. Secure communication over the Web relies on HTTPS, which requires the use of a digital certificate that lets browsers verify the identity of web servers (e.g., is that really google.com?). Web servers obtain their certificates from trusted third parties called certificate authorities (CAs). Certbot is an easy-to-use client that fetches a certificate from Let’s Encrypt—an open certificate authority launched by the EFF, Mozilla, and others—and deploys it to a web server. Anyone who has gone through the trouble of setting up a secure website knows what a hassle getting and maintaining a certificate is. Certbot and Let’s Encrypt can automate away the pain and let you turn on and manage HTTPS with simple commands. Using Certbot and Let's Encrypt is free. .. _installation: Getting Started --------------- The best way to get started is to use our `interactive guide `_. It generates instructions based on your configuration settings. In most cases, you’ll need `root or administrator access `_ to your web server to run Certbot. Certbot is meant to be run directly on your web server on the command line, not on your personal computer. If you’re using a hosted service and don’t have direct access to your web server, you might not be able to use Certbot. Check with your hosting provider for documentation about uploading certificates or using certificates issued by Let’s Encrypt. Contributing ------------ If you'd like to contribute to this project please read `Developer Guide `_. This project is governed by `EFF's Public Projects Code of Conduct `_. Links ===== .. Do not modify this comment unless you know what you're doing. tag:links-begin Documentation: https://certbot.eff.org/docs Software project: https://github.com/certbot/certbot Changelog: https://github.com/certbot/certbot/blob/master/certbot/CHANGELOG.md For Contributors: https://certbot.eff.org/docs/contributing.html For Users: https://certbot.eff.org/docs/using.html Main Website: https://certbot.eff.org Let's Encrypt Website: https://letsencrypt.org Community: https://community.letsencrypt.org ACME spec: `RFC 8555 `_ ACME working area in github (archived): https://github.com/ietf-wg-acme/acme .. Do not modify this comment unless you know what you're doing. tag:links-end .. Do not modify this comment unless you know what you're doing. tag:intro-end .. Do not modify this comment unless you know what you're doing. tag:features-begin Current Features ===================== * Supports multiple web servers: - Apache 2.4+ - nginx/0.8.48+ - webroot (adds files to webroot directories in order to prove control of domains and obtain certificates) - standalone (runs its own simple webserver to prove you control a domain) - other server software via `third party plugins `_ * The private key is generated locally on your system. * Can talk to the Let's Encrypt CA or optionally to other ACME compliant services. * Can get domain-validated (DV) certificates. * Can revoke certificates. * Supports ECDSA (default) and RSA certificate private keys. * Can optionally install a http -> https redirect, so your site effectively runs https only. * Fully automated. * Configuration changes are logged and can be reverted. .. Do not modify this comment unless you know what you're doing. tag:features-end ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3270836 certbot-2.9.0/certbot/0000775000175100017510000000000014561227516013633 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421518.0 certbot-2.9.0/certbot/__init__.py0000664000175100017510000000016114561227516015742 0ustar00ericaerica"""Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 __version__ = '2.9.0' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3270836 certbot-2.9.0/certbot/_internal/0000775000175100017510000000000014561227516015606 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/__init__.py0000664000175100017510000000027014561227515017715 0ustar00ericaerica""" Modules internal to Certbot. This package contains modules that are not considered part of Certbot's public API. They may be changed without updating Certbot's major version. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/account.py0000664000175100017510000003446214561227515017624 0ustar00ericaerica"""Creates ACME accounts for server.""" import datetime import functools import hashlib import logging import shutil import socket from typing import Any from typing import Callable from typing import cast from typing import Dict from typing import List from typing import Mapping from typing import Optional from cryptography.hazmat.primitives import serialization import josepy as jose import pyrfc3339 import pytz from acme import fields as acme_fields from acme import messages from acme.client import ClientV2 from certbot import configuration from certbot import errors from certbot import interfaces from certbot import util from certbot._internal import constants from certbot.compat import filesystem from certbot.compat import os logger = logging.getLogger(__name__) class Account: """ACME protocol registration. :ivar .RegistrationResource regr: Registration Resource :ivar .JWK key: Authorized Account Key :ivar .Meta: Account metadata :ivar str id: Globally unique account identifier. """ class Meta(jose.JSONObjectWithFields): """Account metadata :ivar datetime.datetime creation_dt: Creation date and time (UTC). :ivar str creation_host: FQDN of host, where account has been created. :ivar str register_to_eff: If not None, Certbot will register the provided email during the account registration. .. note:: ``creation_dt`` and ``creation_host`` are useful in cross-machine migration scenarios. """ creation_dt: datetime.datetime = acme_fields.rfc3339("creation_dt") creation_host: str = jose.field("creation_host") register_to_eff: str = jose.field("register_to_eff", omitempty=True) def __init__(self, regr: messages.RegistrationResource, key: jose.JWK, meta: Optional['Meta'] = None) -> None: self.key = key self.regr = regr self.meta = self.Meta( # pyrfc3339 drops microseconds, make sure __eq__ is sane creation_dt=datetime.datetime.now(tz=pytz.UTC).replace(microsecond=0), creation_host=socket.getfqdn(), register_to_eff=None) if meta is None else meta # try MD5, else use MD5 in non-security mode (e.g. for FIPS systems / RHEL) try: hasher = hashlib.md5() except ValueError: # This cast + dictionary expansion is made to make mypy happy without the need of a # "type: ignore" directive that will also require to disable the check on useless # "type: ignore" directives when mypy is run on Python 3.9+. hasher = hashlib.new('md5', **cast(Mapping[str, Any], {"usedforsecurity": False})) hasher.update(self.key.key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo) ) self.id = hasher.hexdigest() # Implementation note: Email? Multiple accounts can have the # same email address. Registration URI? Assigned by the # server, not guaranteed to be stable over time, nor # canonical URI can be generated. ACME protocol doesn't allow # account key (and thus its fingerprint) to be updated... @property def slug(self) -> str: """Short account identification string, useful for UI.""" return "{1}@{0} ({2})".format(pyrfc3339.generate( self.meta.creation_dt), self.meta.creation_host, self.id[:4]) def __repr__(self) -> str: return "<{0}({1}, {2}, {3})>".format( self.__class__.__name__, self.regr, self.id, self.meta) def __eq__(self, other: Any) -> bool: return (isinstance(other, self.__class__) and self.key == other.key and self.regr == other.regr and self.meta == other.meta) class AccountMemoryStorage(interfaces.AccountStorage): """In-memory account storage.""" def __init__(self, initial_accounts: Optional[Dict[str, Account]] = None) -> None: self.accounts = initial_accounts if initial_accounts is not None else {} def find_all(self) -> List[Account]: return list(self.accounts.values()) def save(self, account: Account, client: ClientV2) -> None: if account.id in self.accounts: logger.debug("Overwriting account: %s", account.id) self.accounts[account.id] = account def load(self, account_id: str) -> Account: try: return self.accounts[account_id] except KeyError: raise errors.AccountNotFound(account_id) class AccountFileStorage(interfaces.AccountStorage): """Accounts file storage. :ivar certbot.configuration.NamespaceConfig config: Client configuration """ def __init__(self, config: configuration.NamespaceConfig) -> None: self.config = config util.make_or_verify_dir(config.accounts_dir, 0o700, self.config.strict_permissions) def _account_dir_path(self, account_id: str) -> str: return self._account_dir_path_for_server_path(account_id, self.config.server_path) def _account_dir_path_for_server_path(self, account_id: str, server_path: str) -> str: accounts_dir = self.config.accounts_dir_for_server_path(server_path) return os.path.join(accounts_dir, account_id) @classmethod def _regr_path(cls, account_dir_path: str) -> str: return os.path.join(account_dir_path, "regr.json") @classmethod def _key_path(cls, account_dir_path: str) -> str: return os.path.join(account_dir_path, "private_key.json") @classmethod def _metadata_path(cls, account_dir_path: str) -> str: return os.path.join(account_dir_path, "meta.json") def _find_all_for_server_path(self, server_path: str) -> List[Account]: accounts_dir = self.config.accounts_dir_for_server_path(server_path) try: candidates = os.listdir(accounts_dir) except OSError: return [] accounts = [] for account_id in candidates: try: accounts.append(self._load_for_server_path(account_id, server_path)) except errors.AccountStorageError: logger.debug("Account loading problem", exc_info=True) if not accounts and server_path in constants.LE_REUSE_SERVERS: # find all for the next link down prev_server_path = constants.LE_REUSE_SERVERS[server_path] prev_accounts = self._find_all_for_server_path(prev_server_path) # if we found something, link to that if prev_accounts: try: self._symlink_to_accounts_dir(prev_server_path, server_path) except OSError: return [] accounts = prev_accounts return accounts def find_all(self) -> List[Account]: return self._find_all_for_server_path(self.config.server_path) def _symlink_to_account_dir(self, prev_server_path: str, server_path: str, account_id: str) -> None: prev_account_dir = self._account_dir_path_for_server_path(account_id, prev_server_path) new_account_dir = self._account_dir_path_for_server_path(account_id, server_path) os.symlink(prev_account_dir, new_account_dir) def _symlink_to_accounts_dir(self, prev_server_path: str, server_path: str) -> None: accounts_dir = self.config.accounts_dir_for_server_path(server_path) if os.path.islink(accounts_dir): os.unlink(accounts_dir) else: os.rmdir(accounts_dir) prev_account_dir = self.config.accounts_dir_for_server_path(prev_server_path) os.symlink(prev_account_dir, accounts_dir) def _load_for_server_path(self, account_id: str, server_path: str) -> Account: account_dir_path = self._account_dir_path_for_server_path(account_id, server_path) if not os.path.isdir(account_dir_path): # isdir is also true for symlinks if server_path in constants.LE_REUSE_SERVERS: prev_server_path = constants.LE_REUSE_SERVERS[server_path] prev_loaded_account = self._load_for_server_path(account_id, prev_server_path) # we didn't error so we found something, so create a symlink to that accounts_dir = self.config.accounts_dir_for_server_path(server_path) # If accounts_dir isn't empty, make an account specific symlink if os.listdir(accounts_dir): self._symlink_to_account_dir(prev_server_path, server_path, account_id) else: self._symlink_to_accounts_dir(prev_server_path, server_path) return prev_loaded_account raise errors.AccountNotFound(f"Account at {account_dir_path} does not exist") try: with open(self._regr_path(account_dir_path)) as regr_file: regr = messages.RegistrationResource.json_loads(regr_file.read()) with open(self._key_path(account_dir_path)) as key_file: key = jose.JWK.json_loads(key_file.read()) with open(self._metadata_path(account_dir_path)) as metadata_file: meta = Account.Meta.json_loads(metadata_file.read()) except IOError as error: raise errors.AccountStorageError(error) return Account(regr, key, meta) def load(self, account_id: str) -> Account: return self._load_for_server_path(account_id, self.config.server_path) def save(self, account: Account, client: ClientV2) -> None: """Create a new account. :param Account account: account to create :param ClientV2 client: ACME client associated to the account """ try: dir_path = self._prepare(account) self._create(account, dir_path) self._update_meta(account, dir_path) self._update_regr(account, dir_path) except IOError as error: raise errors.AccountStorageError(error) def update_regr(self, account: Account) -> None: """Update the registration resource. :param Account account: account to update """ try: dir_path = self._prepare(account) self._update_regr(account, dir_path) except IOError as error: raise errors.AccountStorageError(error) def update_meta(self, account: Account) -> None: """Update the meta resource. :param Account account: account to update """ try: dir_path = self._prepare(account) self._update_meta(account, dir_path) except IOError as error: raise errors.AccountStorageError(error) def delete(self, account_id: str) -> None: """Delete registration info from disk :param account_id: id of account which should be deleted """ account_dir_path = self._account_dir_path(account_id) if not os.path.isdir(account_dir_path): raise errors.AccountNotFound(f"Account at {account_dir_path} does not exist") # Step 1: Delete account specific links and the directory self._delete_account_dir_for_server_path(account_id, self.config.server_path) # Step 2: Remove any accounts links and directories that are now empty if not os.listdir(self.config.accounts_dir): self._delete_accounts_dir_for_server_path(self.config.server_path) def _delete_account_dir_for_server_path(self, account_id: str, server_path: str) -> None: link_func = functools.partial(self._account_dir_path_for_server_path, account_id) nonsymlinked_dir = self._delete_links_and_find_target_dir(server_path, link_func) shutil.rmtree(nonsymlinked_dir) def _delete_accounts_dir_for_server_path(self, server_path: str) -> None: link_func = self.config.accounts_dir_for_server_path nonsymlinked_dir = self._delete_links_and_find_target_dir(server_path, link_func) os.rmdir(nonsymlinked_dir) def _delete_links_and_find_target_dir(self, server_path: str, link_func: Callable[[str], str]) -> str: """Delete symlinks and return the nonsymlinked directory path. :param str server_path: file path based on server :param callable link_func: callable that returns possible links given a server_path :returns: the final, non-symlinked target :rtype: str """ dir_path = link_func(server_path) # does an appropriate directory link to me? if so, make sure that's gone reused_servers = {} for k, v in constants.LE_REUSE_SERVERS.items(): reused_servers[v] = k # is there a next one up? possible_next_link = True while possible_next_link: possible_next_link = False if server_path in reused_servers: next_server_path = reused_servers[server_path] next_dir_path = link_func(next_server_path) if os.path.islink(next_dir_path) and filesystem.readlink(next_dir_path) == dir_path: possible_next_link = True server_path = next_server_path dir_path = next_dir_path # if there's not a next one up to delete, then delete me # and whatever I link to while os.path.islink(dir_path): target = filesystem.readlink(dir_path) os.unlink(dir_path) dir_path = target return dir_path def _prepare(self, account: Account) -> str: account_dir_path = self._account_dir_path(account.id) util.make_or_verify_dir(account_dir_path, 0o700, self.config.strict_permissions) return account_dir_path def _create(self, account: Account, dir_path: str) -> None: with util.safe_open(self._key_path(dir_path), "w", chmod=0o400) as key_file: key_file.write(account.key.json_dumps()) def _update_regr(self, account: Account, dir_path: str) -> None: with open(self._regr_path(dir_path), "w") as regr_file: regr = messages.RegistrationResource( body={}, uri=account.regr.uri) regr_file.write(regr.json_dumps()) def _update_meta(self, account: Account, dir_path: str) -> None: with open(self._metadata_path(dir_path), "w") as metadata_file: metadata_file.write(account.meta.json_dumps()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/auth_handler.py0000664000175100017510000005112014561227515020614 0ustar00ericaerica"""ACME AuthHandler.""" import datetime import logging import time from typing import Dict from typing import Iterable from typing import List from typing import Optional from typing import Sequence from typing import Tuple from typing import Type import josepy from requests.models import Response from acme import challenges from acme import client from acme import errors as acme_errors from acme import messages from certbot import achallenges from certbot import configuration from certbot import errors from certbot import interfaces from certbot._internal import error_handler from certbot._internal.account import Account from certbot.display import util as display_util from certbot.plugins import common as plugin_common logger = logging.getLogger(__name__) class AuthHandler: """ACME Authorization Handler for a client. :ivar auth: Authenticator capable of solving :class:`~acme.challenges.Challenge` types :type auth: certbot.interfaces.Authenticator :ivar acme.client.ClientV2 acme_client: ACME client API. :ivar account: Client's Account :type account: :class:`certbot._internal.account.Account` :ivar list pref_challs: sorted user specified preferred challenges type strings with the most preferred challenge listed first """ def __init__(self, auth: interfaces.Authenticator, acme_client: Optional[client.ClientV2], account: Optional[Account], pref_challs: List[str]) -> None: self.auth = auth self.acme = acme_client self.account = account self.pref_challs = pref_challs def handle_authorizations(self, orderr: messages.OrderResource, config: configuration.NamespaceConfig, best_effort: bool = False, max_retries: int = 30, max_time_mins: float = 30) -> List[messages.AuthorizationResource]: """ Retrieve all authorizations, perform all challenges required to validate these authorizations, then poll and wait for the authorization to be checked. :param acme.messages.OrderResource orderr: must have authorizations filled in :param certbot.configuration.NamespaceConfig config: current Certbot configuration :param bool best_effort: if True, not all authorizations need to be validated (eg. renew) :param int max_retries: maximum number of retries to poll authorizations :param float max_time_mins: maximum time (in minutes) to poll authorizations :returns: list of all validated authorizations :rtype: List :raises .AuthorizationError: If unable to retrieve all authorizations """ authzrs = orderr.authorizations[:] if not authzrs: raise errors.AuthorizationError('No authorization to handle.') if not self.acme: raise errors.Error("No ACME client defined, authorizations cannot be handled.") # Retrieve challenges that need to be performed to validate authorizations. achalls = self._choose_challenges(authzrs) if not achalls: return authzrs # Starting now, challenges will be cleaned at the end no matter what. with error_handler.ExitHandler(self._cleanup_challenges, achalls): # To begin, let's ask the authenticator plugin to perform all challenges. try: resps = self.auth.perform(achalls) # If debug is on, wait for user input before starting the verification process. if config.debug_challenges: display_util.notification( 'Challenges loaded. Press continue to submit to CA.\n' + self._debug_challenges_msg(achalls, config), pause=True) except errors.AuthorizationError as error: logger.critical('Failure in setting up challenges.') logger.info('Attempting to clean up outstanding challenges...') raise error # All challenges should have been processed by the authenticator. assert len(resps) == len(achalls), 'Some challenges have not been performed.' # Inform the ACME CA server that challenges are available for validation. for achall, resp in zip(achalls, resps): self.acme.answer_challenge(achall.challb, resp) # Wait for authorizations to be checked. logger.info('Waiting for verification...') self._poll_authorizations(authzrs, max_retries, max_time_mins, best_effort) # Keep validated authorizations only. If there is none, no certificate can be issued. authzrs_validated = [authzr for authzr in authzrs if authzr.body.status == messages.STATUS_VALID] if not authzrs_validated: raise errors.AuthorizationError('All challenges have failed.') return authzrs_validated raise errors.Error("An unexpected error occurred while handling the authorizations.") def deactivate_valid_authorizations(self, orderr: messages.OrderResource) -> Tuple[List, List]: """ Deactivate all `valid` authorizations in the order, so that they cannot be re-used in subsequent orders. :param messages.OrderResource orderr: must have authorizations filled in :returns: tuple of list of successfully deactivated authorizations, and list of unsuccessfully deactivated authorizations. :rtype: tuple """ if not self.acme: raise errors.Error("No ACME client defined, cannot deactivate valid authorizations.") to_deactivate = [authzr for authzr in orderr.authorizations if authzr.body.status == messages.STATUS_VALID] deactivated = [] failed = [] for authzr in to_deactivate: try: authzr = self.acme.deactivate_authorization(authzr) deactivated.append(authzr) except acme_errors.Error as e: failed.append(authzr) logger.debug('Failed to deactivate authorization %s: %s', authzr.uri, e) return (deactivated, failed) def _poll_authorizations(self, authzrs: List[messages.AuthorizationResource], max_retries: int, deadline_minutes: float, best_effort: bool) -> None: """ Poll the ACME CA server, to wait for confirmation that authorizations have their challenges all verified. The poll may occur several times, until all authorizations are checked (valid or invalid), or a maximum of retries, or the polling deadline is reached. """ if not self.acme: raise errors.Error("No ACME client defined, cannot poll authorizations.") authzrs_to_check: Dict[int, Tuple[messages.AuthorizationResource, Optional[Response]]] = {index: (authzr, None) for index, authzr in enumerate(authzrs)} authzrs_failed_to_report = [] deadline = datetime.datetime.now() + datetime.timedelta(minutes=deadline_minutes) # Give an initial second to the ACME CA server to check the authorizations sleep_seconds: float = 1 for _ in range(max_retries): # Wait for appropriate time (from Retry-After, initial wait, or no wait) if sleep_seconds > 0: time.sleep(sleep_seconds) # Poll all updated authorizations. authzrs_to_check = {index: self.acme.poll(authzr) for index, (authzr, _) in authzrs_to_check.items()} # Update the original list of authzr with the updated authzrs from server. for index, (authzr, _) in authzrs_to_check.items(): authzrs[index] = authzr # Gather failed authorizations authzrs_failed = [authzr for authzr, _ in authzrs_to_check.values() if authzr.body.status == messages.STATUS_INVALID] for authzr_failed in authzrs_failed: logger.info('Challenge failed for domain %s', authzr_failed.body.identifier.value) # Accumulating all failed authzrs to build a consolidated report # on them at the end of the polling. authzrs_failed_to_report.extend(authzrs_failed) # Extract out the authorization already checked for next poll iteration. # Poll may stop here because there is no pending authorizations anymore. authzrs_to_check = {index: (authzr, resp) for index, (authzr, resp) in authzrs_to_check.items() if authzr.body.status == messages.STATUS_PENDING} if not authzrs_to_check or datetime.datetime.now() > deadline: # Polling process is finished, we can leave the loop break # Be merciful with the ACME server CA, check the Retry-After header returned, # and wait this time before polling again in next loop iteration. # From all the pending authorizations, we take the greatest Retry-After value # to avoid polling an authorization before its relevant Retry-After value. # (by construction resp cannot be None at that time, but mypy do not know it). retry_after = max(self.acme.retry_after(resp, 3) for _, resp in authzrs_to_check.values() if resp is not None) # Whatever Retry-After the ACME server requests, the polling must not take # longer than the overall deadline (https://github.com/certbot/certbot/issues/9526). retry_after = min(retry_after, deadline) sleep_seconds = (retry_after - datetime.datetime.now()).total_seconds() # In case of failed authzrs, create a report to the user. if authzrs_failed_to_report: self._report_failed_authzrs(authzrs_failed_to_report) if not best_effort: # Without best effort, having failed authzrs is critical and fail the process. raise errors.AuthorizationError('Some challenges have failed.') if authzrs_to_check: # Here authzrs_to_check is still not empty, meaning we exceeded the max polling attempt. raise errors.AuthorizationError('All authorizations were not finalized by the CA.') def _choose_challenges(self, authzrs: Iterable[messages.AuthorizationResource] ) -> List[achallenges.AnnotatedChallenge]: """ Retrieve necessary and pending challenges to satisfy server. NB: Necessary and already validated challenges are not retrieved, as they can be reused for a certificate issuance. """ if not self.acme: raise errors.Error("No ACME client defined, cannot choose the challenges.") pending_authzrs = [authzr for authzr in authzrs if authzr.body.status != messages.STATUS_VALID] achalls: List[achallenges.AnnotatedChallenge] = [] if pending_authzrs: logger.info("Performing the following challenges:") for authzr in pending_authzrs: authzr_challenges = authzr.body.challenges path = gen_challenge_path( authzr_challenges, self._get_chall_pref(authzr.body.identifier.value)) achalls.extend(self._challenge_factory(authzr, path)) return achalls def _get_chall_pref(self, domain: str) -> List[Type[challenges.Challenge]]: """Return list of challenge preferences. :param str domain: domain for which you are requesting preferences """ chall_prefs = [] # Make sure to make a copy... plugin_pref = self.auth.get_chall_pref(domain) if self.pref_challs: plugin_pref_types = {chall.typ for chall in plugin_pref} for typ in self.pref_challs: if typ in plugin_pref_types: chall_prefs.append(challenges.Challenge.TYPES[typ]) if chall_prefs: return chall_prefs raise errors.AuthorizationError( "None of the preferred challenges " "are supported by the selected plugin") chall_prefs.extend(plugin_pref) return chall_prefs def _cleanup_challenges(self, achalls: List[achallenges.AnnotatedChallenge]) -> None: """Cleanup challenges. :param achalls: annotated challenges to cleanup :type achalls: `list` of :class:`certbot.achallenges.AnnotatedChallenge` """ logger.info("Cleaning up challenges") self.auth.cleanup(achalls) def _challenge_factory(self, authzr: messages.AuthorizationResource, path: Sequence[int]) -> List[achallenges.AnnotatedChallenge]: """Construct Namedtuple Challenges :param messages.AuthorizationResource authzr: authorization :param list path: List of indices from `challenges`. :returns: achalls, list of challenge type :class:`certbot.achallenges.AnnotatedChallenge` :rtype: list :raises .errors.Error: if challenge type is not recognized """ if not self.account: raise errors.Error("Account is not set.") achalls = [] for index in path: challb = authzr.body.challenges[index] achalls.append(challb_to_achall( challb, self.account.key, authzr.body.identifier.value)) return achalls def _report_failed_authzrs(self, failed_authzrs: List[messages.AuthorizationResource]) -> None: """Notifies the user about failed authorizations.""" if not self.account: raise errors.Error("Account is not set.") problems: Dict[str, List[achallenges.AnnotatedChallenge]] = {} failed_achalls = [challb_to_achall(challb, self.account.key, authzr.body.identifier.value) for authzr in failed_authzrs for challb in authzr.body.challenges if challb.error] for achall in failed_achalls: problems.setdefault(achall.error.typ, []).append(achall) msg = ["\nCertbot failed to authenticate some domains " f"(authenticator: {self.auth.name})." " The Certificate Authority reported these problems:"] for _, achalls in sorted(problems.items(), key=lambda item: item[0]): msg.append(_generate_failed_chall_msg(achalls)) # auth_hint will only be called on authenticators that subclass # plugin_common.Plugin. Refer to comment on that function. if failed_achalls and isinstance(self.auth, plugin_common.Plugin): msg.append(f"\nHint: {self.auth.auth_hint(failed_achalls)}\n") display_util.notify("".join(msg)) def _debug_challenges_msg(self, achalls: List[achallenges.AnnotatedChallenge], config: configuration.NamespaceConfig) -> str: """Construct message for debug challenges prompt :param list achalls: A list of :class:`certbot.achallenges.AnnotatedChallenge`. :param certbot.configuration.NamespaceConfig config: current Certbot configuration :returns: Message containing challenge debug info :rtype: str """ if config.verbose_count > 0: msg = [] http01_achalls = {} dns01_achalls = {} for achall in achalls: if isinstance(achall.chall, challenges.HTTP01): http01_achalls[achall.chall.uri(achall.domain)] = ( achall.validation(achall.account_key) + "\n" ) if isinstance(achall.chall, challenges.DNS01): dns01_achalls[achall.validation_domain_name(achall.domain)] = ( achall.validation(achall.account_key) + "\n" ) if http01_achalls: msg.append("The following URLs should be accessible from the " "internet and return the value mentioned:\n") for uri, key_authz in http01_achalls.items(): msg.append(f"URL: {uri}\nExpected value: {key_authz}") if dns01_achalls: msg.append("The following FQDNs should return a TXT resource " "record with the value mentioned:\n") for fqdn, key_authz_hash in dns01_achalls.items(): msg.append(f"FQDN: {fqdn}\nExpected value: {key_authz_hash}") return "\n" + "\n".join(msg) else: return 'Pass "-v" for more info about challenges.' 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) else: return achallenges.Other(challb=challb, domain=domain) def gen_challenge_path(challbs: List[messages.ChallengeBody], preferences: List[Type[challenges.Challenge]]) -> Tuple[int, ...]: """Generate a plan to get authority over the identity. :param tuple challbs: A tuple of challenges (:class:`acme.messages.Challenge`) from :class:`acme.messages.AuthorizationResource` to be fulfilled by the client in order to prove possession of the identifier. :param list preferences: List of challenge preferences for domain (:class:`acme.challenges.Challenge` subclasses) :returns: list of indices from ``challenges``. :rtype: list :raises certbot.errors.AuthorizationError: If a path cannot be created that satisfies the CA given the preferences and combinations. """ chall_cost = {} max_cost = 1 for i, chall_cls in enumerate(preferences): chall_cost[chall_cls] = i max_cost += i # max_cost is now equal to sum(indices) + 1 best_combo: Optional[Tuple[int, ...]] = None # Set above completing all of the available challenges best_combo_cost = max_cost combinations = tuple((i,) for i in range(len(challbs))) combo_total = 0 for combo in combinations: for challenge_index in combo: combo_total += chall_cost.get(challbs[ challenge_index].chall.__class__, max_cost) if combo_total < best_combo_cost: best_combo = combo best_combo_cost = combo_total combo_total = 0 if not best_combo: raise _report_no_chall_path(challbs) return best_combo def _report_no_chall_path(challbs: List[messages.ChallengeBody]) -> errors.AuthorizationError: """Logs and return a raisable error reporting that no satisfiable chall path exists. :param challbs: challenges from the authorization that can't be satisfied :returns: An authorization error :rtype: certbot.errors.AuthorizationError """ msg = ("Client with the currently selected authenticator does not support " "any combination of challenges that will satisfy the CA.") if len(challbs) == 1 and isinstance(challbs[0].chall, challenges.DNS01): msg += ( " You may need to use an authenticator " "plugin that can do challenges over DNS.") logger.critical(msg) return errors.AuthorizationError(msg) def _generate_failed_chall_msg(failed_achalls: List[achallenges.AnnotatedChallenge]) -> str: """Creates a user friendly error message about failed challenges. :param list failed_achalls: A list of failed :class:`certbot.achallenges.AnnotatedChallenge` with the same error type. :returns: A formatted error message for the client. :rtype: str """ error = failed_achalls[0].error typ = error.typ if messages.is_acme_error(error): typ = error.code msg = [] for achall in failed_achalls: msg.append("\n Domain: %s\n Type: %s\n Detail: %s\n" % ( achall.domain, typ, achall.error.detail)) return "".join(msg) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cert_manager.py0000664000175100017510000004424514561227515020617 0ustar00ericaerica"""Tools for managing certificates.""" import datetime import logging import re import traceback from typing import Any from typing import Callable from typing import Iterable from typing import List from typing import Optional from typing import Tuple from typing import TypeVar from typing import Union import pytz from certbot import configuration from certbot import crypto_util from certbot import errors from certbot import ocsp from certbot import util from certbot._internal import storage from certbot.compat import os from certbot.display import util as display_util logger = logging.getLogger(__name__) ################### # Commands ################### def update_live_symlinks(config: configuration.NamespaceConfig) -> None: """Update the certificate file family symlinks to use archive_dir. Use the information in the config file to make symlinks point to the correct archive directory. .. note:: This assumes that the installation is using a Reverter object. :param config: Configuration. :type config: :class:`certbot._internal.configuration.NamespaceConfig` """ for renewal_file in storage.renewal_conf_files(config): storage.RenewableCert(renewal_file, config, update_symlinks=True) 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 certificates(config: configuration.NamespaceConfig) -> None: """Display information about certs configured with Certbot :param config: Configuration. :type config: :class:`certbot._internal.configuration.NamespaceConfig` """ parsed_certs = [] parse_failures = [] for renewal_file in storage.renewal_conf_files(config): try: renewal_candidate = storage.RenewableCert(renewal_file, config) crypto_util.verify_renewable_cert(renewal_candidate) parsed_certs.append(renewal_candidate) except Exception as e: # pylint: disable=broad-except logger.warning("Renewal configuration file %s produced an " "unexpected error: %s. Skipping.", renewal_file, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) parse_failures.append(renewal_file) # Describe all the certs _describe_certs(config, parsed_certs, parse_failures) def delete(config: configuration.NamespaceConfig) -> None: """Delete Certbot files associated with a certificate lineage.""" certnames = get_certnames(config, "delete", allow_multiple=True) msg = ["The following certificate(s) are selected for deletion:\n"] for certname in certnames: msg.append(" * " + certname) msg.append( "\nWARNING: Before continuing, ensure that the listed certificates are not being used " "by any installed server software (e.g. Apache, nginx, mail servers). Deleting a " "certificate that is still being used will cause the server software to stop working. " "See https://certbot.org/deleting-certs for information on deleting certificates safely." ) msg.append("\nAre you sure you want to delete the above certificate(s)?") if not display_util.yesno("\n".join(msg), default=True): logger.info("Deletion of certificate(s) canceled.") return for certname in certnames: storage.delete_files(config, certname) display_util.notify("Deleted all files relating to certificate {0}." .format(certname)) ################### # Public Helpers ################### def lineage_for_certname(cli_config: configuration.NamespaceConfig, certname: str) -> Optional[storage.RenewableCert]: """Find a lineage object with name certname.""" configs_dir = cli_config.renewal_configs_dir # Verify the directory is there util.make_or_verify_dir(configs_dir, mode=0o755) try: renewal_file = storage.renewal_file_for_certname(cli_config, certname) except errors.CertStorageError: return None try: return storage.RenewableCert(renewal_file, cli_config) except (errors.CertStorageError, IOError): logger.debug("Renewal conf file %s is broken.", renewal_file) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None def domains_for_certname(config: configuration.NamespaceConfig, certname: str) -> Optional[List[str]]: """Find the domains in the cert with name certname.""" lineage = lineage_for_certname(config, certname) return lineage.names() if lineage else None def find_duplicative_certs(config: configuration.NamespaceConfig, domains: List[str]) -> Tuple[Optional[storage.RenewableCert], Optional[storage.RenewableCert]]: """Find existing certs that match the given domain names. This function searches for certificates whose domains are equal to the `domains` parameter and certificates whose domains are a subset of the domains in the `domains` parameter. If multiple certificates are found whose names are a subset of `domains`, the one whose names are the largest subset of `domains` is returned. If multiple certificates' domains are an exact match or equally sized subsets, which matching certificates are returned is undefined. :param config: Configuration. :type config: :class:`certbot._internal.configuration.NamespaceConfig` :param domains: List of domain names :type domains: `list` of `str` :returns: lineages representing the identically matching cert and the largest subset if they exist :rtype: `tuple` of `storage.RenewableCert` or `None` """ def update_certs_for_domain_matches(candidate_lineage: storage.RenewableCert, rv: Tuple[Optional[storage.RenewableCert], Optional[storage.RenewableCert]] ) -> Tuple[Optional[storage.RenewableCert], Optional[storage.RenewableCert]]: """Return cert as identical_names_cert if it matches, or subset_names_cert if it matches as subset """ # TODO: Handle these differently depending on whether they are # expired or still valid? identical_names_cert, subset_names_cert = rv candidate_names = set(candidate_lineage.names()) if candidate_names == set(domains): identical_names_cert = candidate_lineage elif candidate_names.issubset(set(domains)): # This logic finds and returns the largest subset-names cert # in the case where there are several available. if subset_names_cert is None: subset_names_cert = candidate_lineage elif len(candidate_names) > len(subset_names_cert.names()): subset_names_cert = candidate_lineage return (identical_names_cert, subset_names_cert) init: Tuple[Optional[storage.RenewableCert], Optional[storage.RenewableCert]] = (None, None) return _search_lineages(config, update_certs_for_domain_matches, init) def _archive_files(candidate_lineage: storage.RenewableCert, filetype: str) -> Optional[List[str]]: """ In order to match things like: /etc/letsencrypt/archive/example.com/chain1.pem. Anonymous functions which call this function are eventually passed (in a list) to `match_and_check_overlaps` to help specify the acceptable_matches. :param `.storage.RenewableCert` candidate_lineage: Lineage whose archive dir is to be searched. :param str filetype: main file name prefix e.g. "fullchain" or "chain". :returns: Files in candidate_lineage's archive dir that match the provided filetype. :rtype: list of str or None """ archive_dir = candidate_lineage.archive_dir pattern = [os.path.join(archive_dir, f) for f in os.listdir(archive_dir) if re.match("{0}[0-9]*.pem".format(filetype), f)] if pattern: return pattern return None def _acceptable_matches() -> List[Union[Callable[[storage.RenewableCert], str], Callable[[storage.RenewableCert], Optional[List[str]]]]]: """ Generates the list that's passed to match_and_check_overlaps. Is its own function to make unit testing easier. :returns: list of functions :rtype: list """ return [lambda x: x.fullchain_path, lambda x: x.cert_path, lambda x: _archive_files(x, "cert"), lambda x: _archive_files(x, "fullchain")] def cert_path_to_lineage(cli_config: configuration.NamespaceConfig) -> str: """ If config.cert_path is defined, try to find an appropriate value for config.certname. :param `configuration.NamespaceConfig` cli_config: parsed command line arguments :returns: a lineage name :rtype: str :raises `errors.Error`: If the specified cert path can't be matched to a lineage name. :raises `errors.OverlappingMatchFound`: If the matched lineage's archive is shared. """ acceptable_matches = _acceptable_matches() match = match_and_check_overlaps(cli_config, acceptable_matches, lambda x: cli_config.cert_path, lambda x: x.lineagename) return match[0] 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 human_readable_cert_info(config: configuration.NamespaceConfig, cert: storage.RenewableCert, skip_filter_checks: bool = False) -> Optional[str]: """ Returns a human readable description of info about a RenewableCert object""" certinfo = [] checker = ocsp.RevocationChecker() if config.certname and cert.lineagename != config.certname and not skip_filter_checks: return None if config.domains and not set(config.domains).issubset(cert.names()): return None now = datetime.datetime.now(pytz.UTC) reasons = [] if cert.is_test_cert: reasons.append('TEST_CERT') if cert.target_expiry <= now: reasons.append('EXPIRED') elif checker.ocsp_revoked(cert): reasons.append('REVOKED') if reasons: status = "INVALID: " + ", ".join(reasons) else: diff = cert.target_expiry - now if diff.days == 1: status = "VALID: 1 day" elif diff.days < 1: status = f"VALID: {diff.seconds // 3600} hour(s)" else: status = f"VALID: {diff.days} days" valid_string = "{0} ({1})".format(cert.target_expiry, status) serial = format(crypto_util.get_serial_from_cert(cert.cert_path), 'x') certinfo.append(f" Certificate Name: {cert.lineagename}\n" f" Serial Number: {serial}\n" f" Key Type: {cert.private_key_type}\n" f' Domains: {" ".join(cert.names())}\n' f" Expiry Date: {valid_string}\n" f" Certificate Path: {cert.fullchain}\n" f" Private Key Path: {cert.privkey}") return "".join(certinfo) def get_certnames(config: configuration.NamespaceConfig, verb: str, allow_multiple: bool = False, custom_prompt: Optional[str] = None) -> List[str]: """Get certname from flag, interactively, or error out.""" certname = config.certname if certname: certnames = [certname] else: filenames = storage.renewal_conf_files(config) choices = [storage.lineagename_for_filename(name) for name in filenames] if not choices: raise errors.Error("No existing certificates found.") if allow_multiple: if not custom_prompt: prompt = "Which certificate(s) would you like to {0}?".format(verb) else: prompt = custom_prompt code, certnames = display_util.checklist( prompt, choices, cli_flag="--cert-name", force_interactive=True) if code != display_util.OK: raise errors.Error("User ended interaction.") else: if not custom_prompt: prompt = "Which certificate would you like to {0}?".format(verb) else: prompt = custom_prompt code, index = display_util.menu( prompt, choices, cli_flag="--cert-name", force_interactive=True) if code != display_util.OK or index not in range(0, len(choices)): raise errors.Error("User ended interaction.") certnames = [choices[index]] return certnames ################### # Private Helpers ################### def _report_lines(msgs: Iterable[str]) -> str: """Format a results report for a category of single-line renewal outcomes""" return " " + "\n ".join(str(msg) for msg in msgs) def _report_human_readable(config: configuration.NamespaceConfig, parsed_certs: Iterable[storage.RenewableCert]) -> str: """Format a results report for a parsed cert""" certinfo = [] for cert in parsed_certs: cert_info = human_readable_cert_info(config, cert) if cert_info is not None: certinfo.append(cert_info) return "\n".join(certinfo) def _describe_certs(config: configuration.NamespaceConfig, parsed_certs: Iterable[storage.RenewableCert], parse_failures: Iterable[str]) -> None: """Print information about the certs we know about""" out: List[str] = [] notify = out.append if not parsed_certs and not parse_failures: notify("No certificates found.") else: if parsed_certs: match = "matching " if config.certname or config.domains else "" notify("Found the following {0}certs:".format(match)) notify(_report_human_readable(config, parsed_certs)) if parse_failures: notify("\nThe following renewal configurations " "were invalid:") notify(_report_lines(parse_failures)) display_util.notification("\n".join(out), pause=False, wrap=False) T = TypeVar('T') def _search_lineages(cli_config: configuration.NamespaceConfig, func: Callable[..., T], initial_rv: T, *args: Any) -> T: """Iterate func over unbroken lineages, allowing custom return conditions. Allows flexible customization of return values, including multiple return values and complex checks. :param `configuration.NamespaceConfig` cli_config: parsed command line arguments :param function func: function used while searching over lineages :param initial_rv: initial return value of the function (any type) :returns: Whatever was specified by `func` if a match is found. """ configs_dir = cli_config.renewal_configs_dir # Verify the directory is there util.make_or_verify_dir(configs_dir, mode=0o755) rv = initial_rv for renewal_file in storage.renewal_conf_files(cli_config): try: candidate_lineage = storage.RenewableCert(renewal_file, cli_config) except (errors.CertStorageError, IOError): logger.debug("Renewal conf file %s is broken. Skipping.", renewal_file) logger.debug("Traceback was:\n%s", traceback.format_exc()) continue rv = func(candidate_lineage, rv, *args) return rv ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3270836 certbot-2.9.0/certbot/_internal/cli/0000775000175100017510000000000014561227516016355 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cli/__init__.py0000664000175100017510000006250614561227515020476 0ustar00ericaerica"""Certbot command line argument & config processing.""" # pylint: disable=too-many-lines import argparse import logging import logging.handlers import sys from typing import Any from typing import List from typing import Optional from typing import Type import certbot from certbot.configuration import NamespaceConfig from certbot._internal import constants from certbot._internal.cli.cli_constants import ARGPARSE_PARAMS_TO_REMOVE from certbot._internal.cli.cli_constants import cli_command from certbot._internal.cli.cli_constants import COMMAND_OVERVIEW from certbot._internal.cli.cli_constants import DEPRECATED_OPTIONS from certbot._internal.cli.cli_constants import EXIT_ACTIONS from certbot._internal.cli.cli_constants import HELP_AND_VERSION_USAGE from certbot._internal.cli.cli_constants import SHORT_USAGE from certbot._internal.cli.cli_constants import VAR_MODIFIERS from certbot._internal.cli.cli_constants import ZERO_ARG_ACTIONS from certbot._internal.cli.cli_utils import _DeployHookAction from certbot._internal.cli.cli_utils import _DomainsAction from certbot._internal.cli.cli_utils import _EncodeReasonAction from certbot._internal.cli.cli_utils import _PrefChallAction from certbot._internal.cli.cli_utils import _RenewHookAction from certbot._internal.cli.cli_utils import _user_agent_comment_type from certbot._internal.cli.cli_utils import add_domains from certbot._internal.cli.cli_utils import CaseInsensitiveList from certbot._internal.cli.cli_utils import config_help from certbot._internal.cli.cli_utils import CustomHelpFormatter from certbot._internal.cli.cli_utils import flag_default from certbot._internal.cli.cli_utils import HelpfulArgumentGroup from certbot._internal.cli.cli_utils import nonnegative_int from certbot._internal.cli.cli_utils import parse_preferred_challenges from certbot._internal.cli.cli_utils import read_file from certbot._internal.cli.cli_utils import set_test_server_options from certbot._internal.cli.group_adder import _add_all_groups from certbot._internal.cli.helpful import HelpfulArgumentParser from certbot._internal.cli.paths_parser import _paths_parser from certbot._internal.cli.plugins_parsing import _plugins_parsing from certbot._internal.cli.subparsers import _create_subparsers from certbot._internal.cli.verb_help import VERB_HELP from certbot._internal.cli.verb_help import VERB_HELP_MAP from certbot._internal.plugins import disco as plugins_disco import certbot._internal.plugins.selection as plugin_selection from certbot.plugins import enhancements logger = logging.getLogger(__name__) # Global, to save us from a lot of argument passing within the scope of this module helpful_parser: Optional[HelpfulArgumentParser] = None def prepare_and_parse_args(plugins: plugins_disco.PluginsRegistry, args: List[str] ) -> NamespaceConfig: """Returns parsed command line arguments. :param .PluginsRegistry plugins: available plugins :param list args: command line arguments with the program name removed :returns: parsed command line arguments :rtype: configuration.NamespaceConfig """ helpful = HelpfulArgumentParser(args, plugins) _add_all_groups(helpful) # --help is automatically provided by argparse helpful.add( None, "-v", "--verbose", dest="verbose_count", action="count", default=flag_default("verbose_count"), help="This flag can be used " "multiple times to incrementally increase the verbosity of output, " "e.g. -vvv.") # This is for developers to set the level in the cli.ini, and overrides # the --verbose flag helpful.add( None, "--verbose-level", dest="verbose_level", default=flag_default("verbose_level"), help=argparse.SUPPRESS) helpful.add( None, "-t", "--text", dest="text_mode", action="store_true", default=flag_default("text_mode"), help=argparse.SUPPRESS) helpful.add( None, "--max-log-backups", type=nonnegative_int, default=flag_default("max_log_backups"), help="Specifies the maximum number of backup logs that should " "be kept by Certbot's built in log rotation. Setting this " "flag to 0 disables log rotation entirely, causing " "Certbot to always append to the same log file.") helpful.add( None, "--preconfigured-renewal", dest="preconfigured_renewal", action="store_true", default=flag_default("preconfigured_renewal"), help=argparse.SUPPRESS ) helpful.add( [None, "automation", "run", "certonly", "enhance"], "-n", "--non-interactive", "--noninteractive", dest="noninteractive_mode", action="store_true", default=flag_default("noninteractive_mode"), help="Run without ever asking for user input. This may require " "additional command line flags; the client will try to explain " "which ones are required if it finds one missing") helpful.add( [None, "register", "run", "certonly", "enhance"], constants.FORCE_INTERACTIVE_FLAG, action="store_true", default=flag_default("force_interactive"), help="Force Certbot to be interactive even if it detects it's not " "being run in a terminal. This flag cannot be used with the " "renew subcommand.") helpful.add( [None, "run", "certonly", "certificates", "enhance"], "-d", "--domains", "--domain", dest="domains", metavar="DOMAIN", action=_DomainsAction, default=flag_default("domains"), help="Domain names to include. For multiple domains you can use multiple -d flags " "or enter a comma separated list of domains as a parameter. All domains will " "be included as Subject Alternative Names on the certificate. The first domain " "will be used as the certificate name, unless otherwise specified or if you " "already have a certificate with the same name. In the case of a name conflict, " "a number like -0001 will be appended to the certificate name. (default: Ask)") helpful.add( [None, "run", "certonly", "register"], "--eab-kid", dest="eab_kid", metavar="EAB_KID", help="Key Identifier for External Account Binding" ) helpful.add( [None, "run", "certonly", "register"], "--eab-hmac-key", dest="eab_hmac_key", metavar="EAB_HMAC_KEY", help="HMAC key for External Account Binding" ) helpful.add( [None, "run", "certonly", "manage", "delete", "certificates", "renew", "enhance", "reconfigure"], "--cert-name", dest="certname", metavar="CERTNAME", default=flag_default("certname"), help="Certificate name to apply. This name is used by Certbot for housekeeping " "and in file paths; it doesn't affect the content of the certificate itself. " "Certificate name cannot contain filepath separators (i.e. '/' or '\\', depending " "on the platform). " "To see certificate names, run 'certbot certificates'. " "When creating a new certificate, specifies the new certificate's name. " "(default: the first provided domain or the name of an existing " "certificate on your system for the same domains)") helpful.add( [None, "testing", "renew", "certonly"], "--dry-run", action="store_true", dest="dry_run", default=flag_default("dry_run"), help="Perform a test run against the Let's Encrypt staging server, obtaining test" " (invalid) certificates but not saving them to disk. This can only be used with the" " 'certonly' and 'renew' subcommands. It may trigger webserver reloads to " " temporarily modify & roll back configuration files." " --pre-hook and --post-hook commands run by default." " --deploy-hook commands do not run, unless enabled by --run-deploy-hooks." " The test server may be overridden with --server.") helpful.add( ["testing", "renew", "certonly", "reconfigure"], "--run-deploy-hooks", action="store_true", dest="run_deploy_hooks", default=flag_default("run_deploy_hooks"), help="When performing a test run using `--dry-run` or `reconfigure`, run any applicable" " deploy hooks. This includes hooks set on the command line, saved in the" " certificate's renewal configuration file, or present in the renewal-hooks directory." " To exclude directory hooks, use --no-directory-hooks. The hook(s) will only" " be run if the dry run succeeds, and will use the current active certificate, not" " the temporary test certificate acquired during the dry run. This flag is recommended" " when modifying the deploy hook using `reconfigure`.") helpful.add( ["register", "automation"], "--register-unsafely-without-email", action="store_true", default=flag_default("register_unsafely_without_email"), help="Specifying this flag enables registering an account with no " "email address. This is strongly discouraged, because you will be " "unable to receive notice about impending expiration or " "revocation of your certificates or problems with your Certbot " "installation that will lead to failure to renew.") helpful.add( ["register", "update_account", "unregister", "automation"], "-m", "--email", default=flag_default("email"), help=config_help("email")) helpful.add(["register", "update_account", "automation"], "--eff-email", action="store_true", default=flag_default("eff_email"), dest="eff_email", help="Share your e-mail address with EFF") helpful.add(["register", "update_account", "automation"], "--no-eff-email", action="store_false", default=flag_default("eff_email"), dest="eff_email", help="Don't share your e-mail address with EFF") helpful.add( ["automation", "certonly", "run"], "--keep-until-expiring", "--keep", "--reinstall", dest="reinstall", action="store_true", default=flag_default("reinstall"), help="If the requested certificate matches an existing certificate, always keep the " "existing one until it is due for renewal (for the " "'run' subcommand this means reinstall the existing certificate). (default: Ask)") helpful.add( "automation", "--expand", action="store_true", default=flag_default("expand"), help="If an existing certificate is a strict subset of the requested names, " "always expand and replace it with the additional names. (default: Ask)") helpful.add( "automation", "--version", action="version", version="%(prog)s {0}".format(certbot.__version__), help="show program's version number and exit") helpful.add( ["automation", "renew"], "--force-renewal", "--renew-by-default", dest="renew_by_default", action="store_true", default=flag_default("renew_by_default"), help="If a certificate " "already exists for the requested domains, renew it now, " "regardless of whether it is near expiry. (Often " "--keep-until-expiring is more appropriate). Also implies " "--expand.") helpful.add( "automation", "--renew-with-new-domains", dest="renew_with_new_domains", action="store_true", default=flag_default("renew_with_new_domains"), help="If a " "certificate already exists for the requested certificate name " "but does not match the requested domains, renew it now, " "regardless of whether it is near expiry.") helpful.add( "automation", "--reuse-key", dest="reuse_key", action="store_true", default=flag_default("reuse_key"), help="When renewing, use the same private key as the existing " "certificate.") helpful.add( "automation", "--no-reuse-key", dest="reuse_key", action="store_false", default=flag_default("reuse_key"), help="When renewing, do not use the same private key as the existing " "certificate. Not reusing private keys is the default behavior of " "Certbot. This option may be used to unset --reuse-key on an " "existing certificate.") helpful.add( "automation", "--new-key", dest="new_key", action="store_true", default=flag_default("new_key"), help="When renewing or replacing a certificate, generate a new private key, " "even if --reuse-key is set on the existing certificate. Combining " "--new-key and --reuse-key will result in the private key being replaced and " "then reused in future renewals.") helpful.add( ["automation", "renew", "certonly"], "--allow-subset-of-names", action="store_true", default=flag_default("allow_subset_of_names"), help="When performing domain validation, do not consider it a failure " "if authorizations can not be obtained for a strict subset of " "the requested domains. This may be useful for allowing renewals for " "multiple domains to succeed even if some domains no longer point " "at this system. This option cannot be used with --csr.") helpful.add( "automation", "--agree-tos", dest="tos", action="store_true", default=flag_default("tos"), help="Agree to the ACME Subscriber Agreement (default: Ask)") helpful.add( ["unregister", "automation"], "--account", metavar="ACCOUNT_ID", default=flag_default("account"), help="Account ID to use") helpful.add( "automation", "--duplicate", dest="duplicate", action="store_true", default=flag_default("duplicate"), help="Allow making a certificate lineage that duplicates an existing one " "(both can be renewed in parallel)") helpful.add( ["automation", "renew", "certonly", "run"], "-q", "--quiet", dest="quiet", action="store_true", default=flag_default("quiet"), help="Silence all output except errors. Useful for automation via cron." " Implies --non-interactive.") # overwrites server, handled in HelpfulArgumentParser.parse_args() helpful.add(["testing", "revoke", "run"], "--test-cert", "--staging", dest="staging", action="store_true", default=flag_default("staging"), help="Use the Let's Encrypt staging server to obtain or revoke test (invalid) " "certificates; equivalent to --server " + constants.STAGING_URI) helpful.add( "testing", "--debug", action="store_true", default=flag_default("debug"), help="Show tracebacks in case of errors") helpful.add( [None, "certonly", "run"], "--debug-challenges", action="store_true", default=flag_default("debug_challenges"), help="After setting up challenges, wait for user input before " "submitting to CA. When used in combination with the `-v` " "option, the challenge URLs or FQDNs and their expected " "return values are shown.") helpful.add( "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) helpful.add( ["testing", "standalone", "manual"], "--http-01-port", type=int, dest="http01_port", default=flag_default("http01_port"), help=config_help("http01_port")) helpful.add( ["testing", "standalone"], "--http-01-address", dest="http01_address", default=flag_default("http01_address"), help=config_help("http01_address")) helpful.add( ["testing", "nginx"], "--https-port", type=int, default=flag_default("https_port"), help=config_help("https_port")) helpful.add( "testing", "--break-my-certs", action="store_true", default=flag_default("break_my_certs"), help="Be willing to replace or renew valid certificates with invalid " "(testing/staging) certificates") helpful.add( "security", "--rsa-key-size", type=int, metavar="N", default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) helpful.add( "security", "--key-type", choices=['rsa', 'ecdsa'], type=str, default=flag_default("key_type"), help=config_help("key_type")) helpful.add( "security", "--elliptic-curve", type=str, choices=[ 'secp256r1', 'secp384r1', 'secp521r1', ], metavar="N", default=flag_default("elliptic_curve"), help=config_help("elliptic_curve")) helpful.add( "security", "--must-staple", action="store_true", dest="must_staple", default=flag_default("must_staple"), help=config_help("must_staple")) helpful.add( ["security", "enhance"], "--redirect", action="store_true", dest="redirect", default=flag_default("redirect"), help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost. (default: redirect enabled for install and run, " "disabled for enhance)") helpful.add( "security", "--no-redirect", action="store_false", dest="redirect", default=flag_default("redirect"), help="Do not automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost. (default: redirect enabled for install and run, " "disabled for enhance)") helpful.add( ["security", "enhance"], "--hsts", action="store_true", dest="hsts", default=flag_default("hsts"), help="Add the Strict-Transport-Security header to every HTTP response." " Forcing browser to always use SSL for the domain." " Defends against SSL Stripping.") helpful.add( "security", "--no-hsts", action="store_false", dest="hsts", default=flag_default("hsts"), help=argparse.SUPPRESS) helpful.add( ["security", "enhance"], "--uir", action="store_true", dest="uir", default=flag_default("uir"), help='Add the "Content-Security-Policy: upgrade-insecure-requests"' ' header to every HTTP response. Forcing the browser to use' ' https:// for every http:// resource.') helpful.add( "security", "--no-uir", action="store_false", dest="uir", default=flag_default("uir"), help=argparse.SUPPRESS) helpful.add( "security", "--staple-ocsp", action="store_true", dest="staple", default=flag_default("staple"), help="Enables OCSP Stapling. A valid OCSP response is stapled to" " the certificate that the server offers during TLS.") helpful.add( "security", "--no-staple-ocsp", action="store_false", dest="staple", default=flag_default("staple"), help=argparse.SUPPRESS) helpful.add( "security", "--strict-permissions", action="store_true", default=flag_default("strict_permissions"), help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") helpful.add( [None, "certonly", "renew", "run"], "--preferred-chain", dest="preferred_chain", default=flag_default("preferred_chain"), help=config_help("preferred_chain") ) helpful.add( ["manual", "standalone", "certonly", "renew"], "--preferred-challenges", dest="pref_challs", action=_PrefChallAction, default=flag_default("pref_challs"), help='A sorted, comma delimited list of the preferred challenge to ' 'use during authorization with the most preferred challenge ' 'listed first (Eg, "dns" or "http,dns"). ' 'Not all plugins support all challenges. See ' 'https://certbot.eff.org/docs/using.html#plugins for details. ' 'ACME Challenges are versioned, but if you pick "http" rather ' 'than "http-01", Certbot will select the latest version ' 'automatically.') helpful.add( [None, "certonly", "run"], "--issuance-timeout", type=nonnegative_int, dest="issuance_timeout", default=flag_default("issuance_timeout"), help=config_help("issuance_timeout")) helpful.add( ["renew", "reconfigure"], "--pre-hook", help="Command to be run in a shell before obtaining any certificates." " Unless --disable-hook-validation is used, the command’s first word" " must be the absolute pathname of an executable or one found via the" " PATH environment variable." " Intended primarily for renewal, where it can be used to temporarily" " shut down a webserver that might conflict with the standalone" " plugin. This will only be called if a certificate is actually to be" " obtained/renewed. When renewing several certificates that have" " identical pre-hooks, only the first will be executed.") helpful.add( ["renew", "reconfigure"], "--post-hook", help="Command to be run in a shell after attempting to obtain/renew" " certificates." " Unless --disable-hook-validation is used, the command’s first word" " must be the absolute pathname of an executable or one found via the" " PATH environment variable." " Can be used to deploy renewed certificates, or to" " restart any servers that were stopped by --pre-hook. This is only" " run if an attempt was made to obtain/renew a certificate. If" " multiple renewed certificates have identical post-hooks, only" " one will be run.") helpful.add(["renew", "reconfigure"], "--renew-hook", action=_RenewHookAction, help=argparse.SUPPRESS) helpful.add( "renew", "--no-random-sleep-on-renew", action="store_false", default=flag_default("random_sleep_on_renew"), dest="random_sleep_on_renew", help=argparse.SUPPRESS) helpful.add( ["renew", "reconfigure"], "--deploy-hook", action=_DeployHookAction, help='Command to be run in a shell once for each successfully' ' issued certificate.' ' Unless --disable-hook-validation is used, the command’s first word' ' must be the absolute pathname of an executable or one found via the' ' PATH environment variable.' ' For this command, the shell variable' ' $RENEWED_LINEAGE will point to the config live subdirectory' ' (for example, "/etc/letsencrypt/live/example.com") containing' ' the new certificates and keys; the shell variable' ' $RENEWED_DOMAINS will contain a space-delimited list of' ' renewed certificate domains (for example, "example.com' ' www.example.com")') helpful.add( "renew", "--disable-hook-validation", action="store_false", dest="validate_hooks", default=flag_default("validate_hooks"), help="Ordinarily the commands specified for" " --pre-hook/--post-hook/--deploy-hook will be checked for" " validity, to see if the programs being run are in the $PATH," " so that mistakes can be caught early, even when the hooks" " aren't being run just yet. The validation is rather" " simplistic and fails if you use more advanced shell" " constructs, so you can use this switch to disable it." " (default: False)") helpful.add( "renew", "--no-directory-hooks", action="store_false", default=flag_default("directory_hooks"), dest="directory_hooks", help="Disable running executables found in Certbot's hook directories" " during renewal. (default: False)") helpful.add( "renew", "--disable-renew-updates", action="store_true", default=flag_default("disable_renew_updates"), dest="disable_renew_updates", help="Disable automatic updates to your server configuration that" " would otherwise be done by the selected installer plugin, and triggered" " when the user executes \"certbot renew\", regardless of if the certificate" " is renewed. This setting does not apply to important TLS configuration" " updates.") helpful.add( "renew", "--no-autorenew", action="store_false", default=flag_default("autorenew"), dest="autorenew", help="Disable auto renewal of certificates. (default: False)") # Deprecated arguments helpful.add_deprecated_argument("--os-packages-only", 0) helpful.add_deprecated_argument("--no-self-upgrade", 0) helpful.add_deprecated_argument("--no-bootstrap", 0) helpful.add_deprecated_argument("--no-permissions-check", 0) # Populate the command line parameters for new style enhancements enhancements.populate_cli(helpful.add) _create_subparsers(helpful) _paths_parser(helpful) # _plugins_parsing should be the last thing to act upon the main # parser (--help should display plugin-specific options last) _plugins_parsing(helpful, plugins) global helpful_parser # pylint: disable=global-statement helpful_parser = helpful return helpful.parse_args() def argparse_type(variable: Any) -> Type: """Return our argparse type function for a config variable (default: str)""" # pylint: disable=protected-access if helpful_parser is not None: for action in helpful_parser.actions: if action.type is not None and action.dest == variable: return action.type return str ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cli/cli_constants.py0000664000175100017510000000767014561227515021603 0ustar00ericaerica"""Certbot command line constants""" cli_command = "certbot" # Argparse's help formatting has a lot of unhelpful peculiarities, so we want # to replace as much of it as we can... # This is the stub to include in help generated by argparse SHORT_USAGE = """ {0} [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ... Certbot can obtain and install HTTPS/TLS/SSL certificates. By default, it will attempt to use a webserver both for obtaining and installing the certificate. """.format(cli_command) # This section is used for --help and --help all ; it needs information # about installed plugins to be fully formatted COMMAND_OVERVIEW = """The most common SUBCOMMANDS and flags are: obtain, install, and renew certificates: (default) run Obtain & install a certificate in your current webserver certonly Obtain or renew a certificate, but do not install it renew Renew all previously obtained certificates that are near expiry enhance Add security enhancements to your existing configuration -d DOMAINS Comma-separated list of domains to obtain a certificate for %s --standalone Run a standalone webserver for authentication %s --webroot Place files in a server's webroot folder for authentication --manual Obtain certificates interactively, or using shell script hooks -n Run non-interactively --test-cert Obtain a test certificate from a staging server --dry-run Test "renew" or "certonly" without saving any certificates to disk manage certificates: certificates Display information about certificates you have from Certbot revoke Revoke a certificate (supply --cert-name or --cert-path) delete Delete a certificate (supply --cert-name) reconfigure Update a certificate's configuration (supply --cert-name) manage your account: register Create an ACME account unregister Deactivate an ACME account update_account Update an ACME account show_account Display account details --agree-tos Agree to the ACME server's Subscriber Agreement -m EMAIL Email address for important account notifications """ # This is the short help for certbot --help, where we disable argparse # altogether HELP_AND_VERSION_USAGE = """ More detailed help: -h, --help [TOPIC] print this message, or detailed help on a topic; the available TOPICS are: all, automation, commands, paths, security, testing, or any of the subcommands or plugins (certonly, renew, install, register, nginx, apache, standalone, webroot, etc.) -h all print a detailed help page including all topics --version print the version number """ # These argparse parameters should be removed when detecting defaults. ARGPARSE_PARAMS_TO_REMOVE = ("const", "nargs", "type",) # These sets are used when to help detect options set by the user. EXIT_ACTIONS = {"help", "version",} ZERO_ARG_ACTIONS = {"store_const", "store_true", "store_false", "append_const", "count",} # Maps a config option to a set of config options that may have modified it. # This dictionary is used recursively, so if A modifies B and B modifies C, # it is determined that C was modified by the user if A was modified. VAR_MODIFIERS = {"account": {"server",}, "renew_hook": {"deploy_hook",}, "server": {"dry_run", "staging",}, "webroot_map": {"webroot_path",}} # This is a list of all CLI options that we have ever deprecated. It lets us # opt out of the default detection, which can interact strangely with option # deprecation. See https://github.com/certbot/certbot/issues/8540 for more info. DEPRECATED_OPTIONS = { "manual_public_ip_logging_ok", "os_packages_only", "no_self_upgrade", "no_bootstrap", "no_permissions_check", "dns_route53_propagation_seconds", "certbot_route53:auth_propagation_seconds" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cli/cli_utils.py0000664000175100017510000002602614561227515020723 0ustar00ericaerica"""Certbot command line util function""" import argparse import copy import glob import inspect from typing import Any from typing import Iterable from typing import List from typing import Optional from typing import Sequence from typing import Tuple from typing import TYPE_CHECKING from typing import Union from acme import challenges from certbot import configuration from certbot import errors from certbot import util from certbot._internal import constants from certbot.compat import os if TYPE_CHECKING: from certbot._internal.cli import helpful def read_file(filename: str, mode: str = "rb") -> Tuple[str, Any]: """Returns the given file's contents. :param str filename: path to file :param str mode: open mode (see `open`) :returns: absolute path of filename and its contents :rtype: tuple :raises argparse.ArgumentTypeError: File does not exist or is not readable. """ try: filename = os.path.abspath(filename) with open(filename, mode) as the_file: contents = the_file.read() return filename, contents except IOError as exc: raise argparse.ArgumentTypeError(exc.strerror) def flag_default(name: str) -> Any: """Default value for CLI flag.""" # XXX: this is an internal housekeeping notion of defaults before # argparse has been set up; it is not accurate for all flags. Call it # with caution. Plugin defaults are missing, and some things are using # defaults defined in this file, not in constants.py :( return copy.deepcopy(constants.CLI_DEFAULTS[name]) def config_help(name: str, hidden: bool = False) -> Optional[str]: """Extract the help message for a `configuration.NamespaceConfig` property docstring.""" if hidden: return argparse.SUPPRESS return inspect.getdoc(getattr(configuration.NamespaceConfig, name)) class HelpfulArgumentGroup: """Emulates an argparse group for use with HelpfulArgumentParser. This class is used in the add_group method of HelpfulArgumentParser. Command line arguments can be added to the group, but help suppression and default detection is applied by HelpfulArgumentParser when necessary. """ def __init__(self, helpful_arg_parser: "helpful.HelpfulArgumentParser", topic: str) -> None: self._parser = helpful_arg_parser self._topic = topic def add_argument(self, *args: Any, **kwargs: Any) -> None: """Add a new command line argument to the argument group.""" self._parser.add(self._topic, *args, **kwargs) class CustomHelpFormatter(argparse.HelpFormatter): """This is a clone of ArgumentDefaultsHelpFormatter, with bugfixes. In particular we fix https://bugs.python.org/issue28742 """ def _get_help_string(self, action: argparse.Action) -> Optional[str]: helpstr = action.help if action.help and '%(default)' not in action.help and '(default:' not in action.help: if action.default != argparse.SUPPRESS: defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE] if helpstr and (action.option_strings or action.nargs in defaulting_nargs): helpstr += ' (default: %(default)s)' return helpstr class _DomainsAction(argparse.Action): """Action class for parsing domains.""" def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, domain: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None: """Just wrap add_domains in argparseese.""" add_domains(namespace, str(domain) if domain is not None else None) def add_domains(args_or_config: Union[argparse.Namespace, configuration.NamespaceConfig], domains: Optional[str]) -> List[str]: """Registers new domains to be used during the current client run. Domains are not added to the list of requested domains if they have already been registered. :param args_or_config: parsed command line arguments :type args_or_config: argparse.Namespace or configuration.NamespaceConfig :param str domain: one or more comma separated domains :returns: domains after they have been normalized and validated :rtype: `list` of `str` """ validated_domains: List[str] = [] if not domains: return validated_domains for domain in domains.split(","): domain = util.enforce_domain_sanity(domain.strip()) validated_domains.append(domain) if domain not in args_or_config.domains: args_or_config.domains.append(domain) return validated_domains class CaseInsensitiveList(list): """A list that will ignore case when searching. This class is passed to the `choices` argument of `argparse.add_arguments` through the `helpful` wrapper. It is necessary due to special handling of command line arguments by `set_by_cli` in which the `type_func` is not applied.""" def __contains__(self, element: object) -> bool: if not isinstance(element, str): return False return super().__contains__(element.lower()) def _user_agent_comment_type(value: str) -> str: if "(" in value or ")" in value: raise argparse.ArgumentTypeError("may not contain parentheses") return value class _EncodeReasonAction(argparse.Action): """Action class for parsing revocation reason.""" def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, reason: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None: """Encodes the reason for certificate revocation.""" if reason is None: raise ValueError("Unexpected null reason.") code = constants.REVOCATION_REASONS[str(reason).lower()] setattr(namespace, self.dest, code) def parse_preferred_challenges(pref_challs: Iterable[str]) -> List[str]: """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 class _PrefChallAction(argparse.Action): """Action class for parsing preferred challenges.""" def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, pref_challs: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None: if pref_challs is None: raise ValueError("Unexpected null pref_challs.") try: challs = parse_preferred_challenges(str(pref_challs).split(",")) except errors.Error as error: raise argparse.ArgumentError(self, str(error)) namespace.pref_challs.extend(challs) class _DeployHookAction(argparse.Action): """Action class for parsing deploy hooks.""" def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None: renew_hook_set = namespace.deploy_hook != namespace.renew_hook if renew_hook_set and namespace.renew_hook != values: raise argparse.ArgumentError( self, "conflicts with --renew-hook value") namespace.deploy_hook = namespace.renew_hook = values class _RenewHookAction(argparse.Action): """Action class for parsing renew hooks.""" def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None: deploy_hook_set = namespace.deploy_hook is not None if deploy_hook_set and namespace.deploy_hook != values: raise argparse.ArgumentError( self, "conflicts with --deploy-hook value") namespace.renew_hook = values def nonnegative_int(value: str) -> int: """Converts value to an int and checks that it is not negative. This function should used as the type parameter for argparse arguments. :param str value: value provided on the command line :returns: integer representation of value :rtype: int :raises argparse.ArgumentTypeError: if value isn't a non-negative integer """ try: int_value = int(value) except ValueError: raise argparse.ArgumentTypeError("value must be an integer") if int_value < 0: raise argparse.ArgumentTypeError("value must be non-negative") return int_value def set_test_server_options(verb: str, config: configuration.NamespaceConfig) -> None: """Updates server, break_my_certs, staging, tos, and register_unsafely_without_email in config as necessary to prepare to use the test server. We have --staging/--dry-run; perform sanity check and set config.server :param str verb: subcommand called :param config: parsed command line arguments :type config: configuration.NamespaceConfig :raises errors.Error: if non-default server is used and --staging is set :raises errors.Error: if inapplicable verb is used and --dry-run is set """ # Flag combinations should produce these results: # | --staging | --dry-run | # ------------------------------------------------------------ # | --server acme-v02 | Use staging | Use staging | # | --server acme-staging-v02 | Use staging | Use staging | # | --server | Conflict error | Use | default_servers = (flag_default("server"), constants.STAGING_URI) if config.staging and config.server not in default_servers: raise errors.Error("--server value conflicts with --staging") if config.server == flag_default("server"): config.server = constants.STAGING_URI # If the account has already been loaded (such as by calling reconstitute before this), # clear it so that we don't try to use the prod account on the staging server. config.account = None if config.dry_run: if verb not in ["certonly", "renew", "reconfigure"]: raise errors.Error("--dry-run currently only works with the " "'certonly' or 'renew' subcommands (%r)" % verb) config.break_my_certs = config.staging = True if glob.glob(os.path.join(config.config_dir, constants.ACCOUNTS_DIR, "*")): # The user has a prod account, but might not have a staging # one; we don't want to start trying to perform interactive registration config.tos = True config.register_unsafely_without_email = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cli/group_adder.py0000664000175100017510000000206114561227515021220 0ustar00ericaerica"""This module contains a function to add the groups of arguments for the help display""" from typing import TYPE_CHECKING from certbot._internal.cli.verb_help import VERB_HELP if TYPE_CHECKING: from certbot._internal.cli import helpful def _add_all_groups(helpful: "helpful.HelpfulArgumentParser") -> None: helpful.add_group("automation", description="Flags for automating execution & other tweaks") helpful.add_group("security", description="Security parameters & server settings") helpful.add_group("testing", description="The following flags are meant for testing and integration purposes only.") helpful.add_group("paths", description="Flags for changing execution paths & servers") helpful.add_group("manage", description="Various subcommands and flags are available for managing your certificates:", verbs=["certificates", "delete", "renew", "revoke", "reconfigure"]) # VERBS for verb, docs in VERB_HELP: name = docs.get("realname", verb) helpful.add_group(name, description=docs["opts"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cli/helpful.py0000664000175100017510000005425614561227515020401 0ustar00ericaerica"""Certbot command line argument parser""" import argparse import functools import sys from typing import Any from typing import Dict from typing import Iterable from typing import List from typing import Optional from typing import Tuple from typing import Union import configargparse from certbot import crypto_util from certbot import errors from certbot import util from certbot._internal import constants from certbot._internal import hooks from certbot._internal.cli.cli_constants import COMMAND_OVERVIEW from certbot._internal.cli.cli_constants import HELP_AND_VERSION_USAGE from certbot._internal.cli.cli_constants import SHORT_USAGE from certbot._internal.cli.cli_utils import add_domains from certbot._internal.cli.cli_utils import CustomHelpFormatter from certbot._internal.cli.cli_utils import flag_default from certbot._internal.cli.cli_utils import HelpfulArgumentGroup from certbot._internal.cli.cli_utils import set_test_server_options from certbot._internal.cli.verb_help import VERB_HELP from certbot._internal.cli.verb_help import VERB_HELP_MAP from certbot._internal.display import obj as display_obj from certbot._internal.plugins import disco from certbot.configuration import ArgumentSource from certbot.configuration import NamespaceConfig class HelpfulArgumentParser: """Argparse Wrapper. This class wraps argparse, adding the ability to make --help less verbose, and request help on specific subcategories at a time, eg 'certbot --help security' for security options. """ def __init__(self, args: List[str], plugins: Iterable[str]) -> None: from certbot._internal import main self.VERBS = { "auth": main.certonly, "certonly": main.certonly, "run": main.run, "install": main.install, "plugins": main.plugins_cmd, "register": main.register, "update_account": main.update_account, "show_account": main.show_account, "unregister": main.unregister, "renew": main.renew, "revoke": main.revoke, "rollback": main.rollback, "everything": main.run, "update_symlinks": main.update_symlinks, "certificates": main.certificates, "delete": main.delete, "enhance": main.enhance, "reconfigure": main.reconfigure, } # Get notification function for printing self.notify = display_obj.NoninteractiveDisplay(sys.stdout).notification self.actions: List[configargparse.Action] = [] # List of topics for which additional help can be provided HELP_TOPICS: List[Optional[str]] = ["all", "security", "paths", "automation", "testing"] HELP_TOPICS += list(self.VERBS) + self.COMMANDS_TOPICS + ["manage"] plugin_names: List[Optional[str]] = list(plugins) self.help_topics: List[Optional[str]] = HELP_TOPICS + plugin_names + [None] self.args = args if self.args and self.args[0] == 'help': self.args[0] = '--help' self.determine_verb() help1 = self.prescan_for_flag("-h", self.help_topics) help2 = self.prescan_for_flag("--help", self.help_topics) self.help_arg: Union[str, bool] if isinstance(help1, bool) and isinstance(help2, bool): self.help_arg = help1 or help2 else: self.help_arg = help1 if isinstance(help1, str) else help2 short_usage = self._usage_string(plugins, self.help_arg) self.visible_topics = self.determine_help_topics(self.help_arg) # elements are added by .add_group() self.groups: Dict[str, argparse._ArgumentGroup] = {} self.parser = configargparse.ArgParser( prog="certbot", usage=short_usage, formatter_class=CustomHelpFormatter, args_for_setting_config_path=["-c", "--config"], default_config_files=flag_default("config_files"), config_arg_help_message="path to config file (default: {0})".format( " and ".join(flag_default("config_files")))) # This is the only way to turn off overly verbose config flag documentation self.parser._add_config_file_help = False self.verb: str # Help that are synonyms for --help subcommands COMMANDS_TOPICS = ["command", "commands", "subcommand", "subcommands", "verbs"] def _list_subcommands(self) -> str: longest = max(len(v) for v in VERB_HELP_MAP) text = "The full list of available SUBCOMMANDS is:\n\n" for verb, props in sorted(VERB_HELP): doc = props.get("short", "") text += '{0:<{length}} {1}\n'.format(verb, doc, length=longest) text += "\nYou can get more help on a specific subcommand with --help SUBCOMMAND\n" return text def _usage_string(self, plugins: Iterable[str], help_arg: Union[str, bool]) -> str: """Make usage strings late so that plugins can be initialised late :param plugins: all discovered plugins :param help_arg: False for none; True for --help; "TOPIC" for --help TOPIC :rtype: str :returns: a short usage string for the top of --help TOPIC) """ if "nginx" in plugins: nginx_doc = "--nginx Use the Nginx plugin for authentication & installation" else: nginx_doc = "(the certbot nginx plugin is not installed)" if "apache" in plugins: apache_doc = "--apache Use the Apache plugin for authentication & installation" else: apache_doc = "(the certbot apache plugin is not installed)" usage = SHORT_USAGE if help_arg is True: self.notify(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_AND_VERSION_USAGE) sys.exit(0) elif help_arg in self.COMMANDS_TOPICS: self.notify(usage + self._list_subcommands()) sys.exit(0) elif help_arg == "all": # if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at # the top; if we're doing --help someothertopic, it's OT so it's not usage += COMMAND_OVERVIEW % (apache_doc, nginx_doc) elif isinstance(help_arg, str): custom = VERB_HELP_MAP.get(help_arg, {}).get("usage", None) usage = custom if custom else usage # Only remaining case is help_arg == False, which gives effectively usage == SHORT_USAGE. return usage def remove_config_file_domains_for_renewal(self, config: NamespaceConfig) -> None: """Make "certbot renew" safe if domains are set in cli.ini.""" # Works around https://github.com/certbot/certbot/issues/4096 assert config.argument_sources is not None if (config.argument_sources['domains'] == ArgumentSource.CONFIG_FILE and self.verb == "renew"): config.domains = [] def _build_sources_dict(self) -> Dict[str, ArgumentSource]: # ConfigArgparse's get_source_to_settings_dict doesn't actually create # default entries for each argument with a default value, omitting many # args we'd otherwise care about. So in general, unless an argument was # specified in a config file/environment variable/command line arg, # consider it as having a "default" value result = { action.dest: ArgumentSource.DEFAULT for action in self.actions } source_to_settings_dict: Dict[str, Dict[str, Tuple[configargparse.Action, str]]] source_to_settings_dict = self.parser.get_source_to_settings_dict() # We'll process the sources dict in order of each source's "priority", # i.e. the order in which ConfigArgparse ultimately sets argument # values: # 1. defaults (`result` already has everything marked as such) # 2. config files # 3. env vars (shouldn't be any) # 4. command line def update_result(settings_dict: Dict[str, Tuple[configargparse.Action, str]], source: ArgumentSource) -> None: actions = [self._find_action_for_arg(arg) if action is None else action for arg, (action, _) in settings_dict.items()] result.update({ action.dest: source for action in actions }) # config file sources look like "config_file|" for source_key in source_to_settings_dict: if source_key.startswith('config_file'): update_result(source_to_settings_dict[source_key], ArgumentSource.CONFIG_FILE) update_result(source_to_settings_dict.get('env_var', {}), ArgumentSource.ENV_VAR) # The command line settings dict is weird, so handle it separately if 'command_line' in source_to_settings_dict: settings_dict: Dict[str, Tuple[None, List[str]]] settings_dict = source_to_settings_dict['command_line'] # type: ignore (_, unprocessed_args) = settings_dict[''] args = [] for arg in unprocessed_args: # ignore non-arguments if not arg.startswith('-'): continue # special case for config file argument, which we don't have an action for if arg in ['-c', '--config']: result['config_dir'] = ArgumentSource.COMMAND_LINE continue if '=' in arg: arg = arg.split('=')[0] elif ' ' in arg: arg = arg.split(' ')[0] if arg.startswith('--'): args.append(arg) # for short args (ones that start with a single hyphen), handle # the case of multiple short args together, e.g. "-tvm" else: for short_arg in arg[1:]: args.append(f"-{short_arg}") for arg in args: # find the action corresponding to this arg action = self._find_action_for_arg(arg) result[action.dest] = ArgumentSource.COMMAND_LINE return result def _find_action_for_arg(self, arg: str) -> configargparse.Action: # Finds a configargparse Action which matches the given arg, where arg # can either be preceded by hyphens (as on the command line) or not (as # in config files) # if the argument doesn't have leading hypens, prefix it so it can be # compared directly w/ action option strings if arg[0] != '-': arg = '--' + arg # first, check for exact matches for action in self.actions: if arg in action.option_strings: return action # now check for abbreviated (i.e. prefix) matches for action in self.actions: for option_string in action.option_strings: if option_string.startswith(arg): return action raise AssertionError(f"Action corresponding to argument {arg} is None") def parse_args(self) -> NamespaceConfig: """Parses command line arguments and returns the result. :returns: parsed command line arguments :rtype: configuration.NamespaceConfig """ parsed_args = self.parser.parse_args(self.args) parsed_args.func = self.VERBS[self.verb] parsed_args.verb = self.verb config = NamespaceConfig(parsed_args) config.set_argument_sources(self._build_sources_dict()) self.remove_config_file_domains_for_renewal(config) # Do any post-parsing homework here if self.verb == "renew": if config.force_interactive: raise errors.Error( "{0} cannot be used with renew".format( constants.FORCE_INTERACTIVE_FLAG)) config.noninteractive_mode = True if config.force_interactive and config.noninteractive_mode: raise errors.Error( "Flag for non-interactive mode and {0} conflict".format( constants.FORCE_INTERACTIVE_FLAG)) if config.staging or config.dry_run: self.set_test_server(config) if config.csr: self.handle_csr(config) if config.must_staple and not config.staple: config.staple = True if config.validate_hooks: hooks.validate_hooks(config) if config.allow_subset_of_names: if any(util.is_wildcard_domain(d) for d in config.domains): raise errors.Error("Using --allow-subset-of-names with a" " wildcard domain is not supported.") if config.hsts and config.auto_hsts: raise errors.Error( "Parameters --hsts and --auto-hsts cannot be used simultaneously.") if isinstance(config.key_type, list) and len(config.key_type) > 1: raise errors.Error( "Only *one* --key-type type may be provided at this time.") return config def set_test_server(self, config: NamespaceConfig) -> None: """Updates server, break_my_certs, staging, tos, and register_unsafely_without_email in config as necessary to prepare to use the test server.""" return set_test_server_options(self.verb, config) def handle_csr(self, config: NamespaceConfig) -> None: """Process a --csr flag.""" if config.verb != "certonly": raise errors.Error("Currently, a CSR file may only be specified " "when obtaining a new or replacement " "via the certonly command. Please try the " "certonly command instead.") if config.allow_subset_of_names: raise errors.Error("--allow-subset-of-names cannot be used with --csr") csrfile, contents = config.csr[0:2] typ, csr, domains = crypto_util.import_csr_file(csrfile, contents) # This is not necessary for webroot to work, however, # obtain_certificate_from_csr requires config.domains to be set for domain in domains: add_domains(config, domain) if not domains: # TODO: add CN to domains instead: raise errors.Error( "Unfortunately, your CSR %s needs to have a SubjectAltName for every domain" % config.csr[0]) config.actual_csr = (csr, typ) csr_domains = {d.lower() for d in domains} config_domains = set(config.domains) if csr_domains != config_domains: raise errors.ConfigurationError( "Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}" .format(", ".join(csr_domains), ", ".join(config_domains))) def determine_verb(self) -> None: """Determines the verb/subcommand provided by the user. This function works around some of the limitations of argparse. """ if "-h" in self.args or "--help" in self.args: # all verbs double as help arguments; don't get them confused self.verb = "help" return for i, token in enumerate(self.args): if token in self.VERBS: verb = token if verb == "auth": verb = "certonly" if verb == "everything": verb = "run" self.verb = verb self.args.pop(i) return self.verb = "run" def prescan_for_flag(self, flag: str, possible_arguments: Iterable[Optional[str]] ) -> Union[str, bool]: """Checks cli input for flags. Check for a flag, which accepts a fixed set of possible arguments, in the command line; we will use this information to configure argparse's help correctly. Return the flag's argument, if it has one that matches the sequence @possible_arguments; otherwise return whether the flag is present. """ if flag not in self.args: return False pos = self.args.index(flag) try: nxt = self.args[pos + 1] if nxt in possible_arguments: return nxt except IndexError: pass return True def add(self, topics: Optional[Union[List[Optional[str]], str]], *args: Any, **kwargs: Any) -> None: """Add a new command line argument. :param topics: str or [str] help topic(s) this should be listed under, or None for options that don't fit under a specific topic which will only be shown in "--help all" output. The first entry determines where the flag lives in the "--help all" output (None -> "optional arguments"). :param list *args: the names of this argument flag :param dict **kwargs: various argparse settings for this argument """ self.actions.append(self._add(topics, *args, **kwargs)) def _add(self, topics: Optional[Union[List[Optional[str]], str]], *args: Any, **kwargs: Any) -> configargparse.Action: action = kwargs.get("action") if action is util.DeprecatedArgumentAction: # If the argument is deprecated through # certbot.util.add_deprecated_argument, it is not shown in the help # output and any value given to the argument is thrown away during # argument parsing. Because of this, we handle this case early # skipping putting the argument in different help topics and # handling default detection since these actions aren't needed and # can cause bugs like # https://github.com/certbot/certbot/issues/8495. return self.parser.add_argument(*args, **kwargs) if isinstance(topics, list): # if this flag can be listed in multiple sections, try to pick the one # that the user has asked for help about topic = self.help_arg if self.help_arg in topics else topics[0] else: topic = topics # there's only one if not isinstance(topic, bool) and self.visible_topics[topic]: if topic in self.groups: group = self.groups[topic] return group.add_argument(*args, **kwargs) else: return self.parser.add_argument(*args, **kwargs) else: kwargs["help"] = argparse.SUPPRESS return self.parser.add_argument(*args, **kwargs) def add_deprecated_argument(self, argument_name: str, num_args: int) -> None: """Adds a deprecated argument with the name argument_name. Deprecated arguments are not shown in the help. If they are used on the command line, a warning is shown stating that the argument is deprecated and no other action is taken. :param str argument_name: Name of deprecated argument. :param int num_args: Number of arguments the option takes. """ # certbot.util.add_deprecated_argument expects the normal add_argument # interface provided by argparse. This is what is given including when # certbot.util.add_deprecated_argument is used by plugins, however, in # that case the first argument to certbot.util.add_deprecated_argument # is certbot._internal.cli.HelpfulArgumentGroup.add_argument which # internally calls the add method of this class. # # The difference between the add method of this class and the standard # argparse add_argument method caused a bug in the past (see # https://github.com/certbot/certbot/issues/8495) so we use the same # code path here for consistency and to ensure it works. To do that, we # wrap the add method in a similar way to # HelpfulArgumentGroup.add_argument by providing a help topic (which in # this case is set to None). add_func = functools.partial(self.add, None) util.add_deprecated_argument(add_func, argument_name, num_args) def add_group(self, topic: str, verbs: Iterable[str] = (), **kwargs: Any) -> HelpfulArgumentGroup: """Create a new argument group. This method must be called once for every topic, however, calls to this function are left next to the argument definitions for clarity. :param str topic: Name of the new argument group. :param str verbs: List of subcommands that should be documented as part of this help group / topic :returns: The new argument group. :rtype: `HelpfulArgumentGroup` """ if self.visible_topics[topic]: self.groups[topic] = self.parser.add_argument_group(topic, **kwargs) if self.help_arg: for v in verbs: self.groups[topic].add_argument(v, help=VERB_HELP_MAP[v]["short"]) return HelpfulArgumentGroup(self, topic) def add_plugin_args(self, plugins: disco.PluginsRegistry) -> None: """ Let each of the plugins add its own command line arguments, which may or may not be displayed as help topics. """ for name, plugin_ep in plugins.items(): parser_or_group = self.add_group(name, description=plugin_ep.long_description) plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name) def determine_help_topics(self, chosen_topic: Union[str, bool] ) -> Dict[Optional[str], bool]: """ The user may have requested help on a topic, return a dict of which topics to display. @chosen_topic has prescan_for_flag's return type :returns: dict """ # topics maps each topic to whether it should be documented by # argparse on the command line if chosen_topic == "auth": chosen_topic = "certonly" if chosen_topic == "everything": chosen_topic = "run" if chosen_topic == "all": # Addition of condition closes #6209 (removal of duplicate route53 option). return {t: t != 'certbot-route53:auth' for t in self.help_topics} elif not chosen_topic: return {t: False for t in self.help_topics} return {t: t == chosen_topic for t in self.help_topics} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cli/paths_parser.py0000664000175100017510000000400714561227515021422 0ustar00ericaerica"""This is a module that adds configuration to the argument parser regarding paths for certificates""" from typing import TYPE_CHECKING from typing import Union from certbot._internal.cli.cli_utils import config_help from certbot._internal.cli.cli_utils import flag_default from certbot.compat import os if TYPE_CHECKING: from certbot._internal.cli import helpful def _paths_parser(helpful: "helpful.HelpfulArgumentParser") -> None: add = helpful.add verb: Union[str, bool] = helpful.verb if verb == "help": verb = helpful.help_arg cpkwargs = { "type": os.path.abspath, "help": "Path to where certificate is saved (with certonly --csr), installed " "from, or revoked" } if verb == "certonly": cpkwargs["default"] = flag_default("auth_cert_path") add(["paths", "install", "revoke", "certonly", "manage"], "--cert-path", **cpkwargs) section = "paths" if isinstance(verb, str) and verb in ("install", "revoke"): section = verb add(section, "--key-path", type=os.path.abspath, help="Path to private key for certificate installation " "or revocation (if account key is missing)") default_cp = None if verb == "certonly": default_cp = flag_default("auth_chain_path") add(["paths", "install"], "--fullchain-path", default=default_cp, type=os.path.abspath, help="Accompanying path to a full certificate chain (certificate plus chain).") add(["paths", "install"], "--chain-path", default=default_cp, type=os.path.abspath, help="Accompanying path to a certificate chain.") add("paths", "--config-dir", default=flag_default("config_dir"), help=config_help("config_dir")) add("paths", "--work-dir", default=flag_default("work_dir"), help=config_help("work_dir")) add("paths", "--logs-dir", default=flag_default("logs_dir"), help="Logs directory.") add(["paths", "show_account"], "--server", default=flag_default("server"), help=config_help("server")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cli/plugins_parsing.py0000664000175100017510000001410214561227515022130 0ustar00ericaerica"""This is a module that handles parsing of plugins for the argument parser""" from typing import TYPE_CHECKING from certbot._internal.cli.cli_utils import flag_default from certbot._internal.plugins import disco if TYPE_CHECKING: from certbot._internal.cli import helpful def _plugins_parsing(helpful: "helpful.HelpfulArgumentParser", plugins: disco.PluginsRegistry) -> None: # It's nuts, but there are two "plugins" topics. Somehow this works helpful.add_group( "plugins", description="Plugin Selection: Certbot client supports an " "extensible plugins architecture. See '%(prog)s plugins' for a " "list of all installed plugins and their names. You can force " "a particular plugin by setting options provided below. Running " "--help will list flags specific to that plugin.") helpful.add("plugins", "--configurator", default=flag_default("configurator"), help="Name of the plugin that is both an authenticator and an installer." " Should not be used together with --authenticator or --installer. " "(default: Ask)") helpful.add(["plugins", "reconfigure"], "-a", "--authenticator", default=flag_default("authenticator"), help="Authenticator plugin name.") helpful.add(["plugins", "reconfigure"], "-i", "--installer", default=flag_default("installer"), help="Installer plugin name (also used to find domains).") helpful.add(["plugins", "certonly", "run", "install"], "--apache", action="store_true", default=flag_default("apache"), help="Obtain and install certificates using Apache") helpful.add(["plugins", "certonly", "run", "install"], "--nginx", action="store_true", default=flag_default("nginx"), help="Obtain and install certificates using Nginx") helpful.add(["plugins", "certonly"], "--standalone", action="store_true", default=flag_default("standalone"), help='Obtain certificates using a "standalone" webserver.') helpful.add(["plugins", "certonly"], "--manual", action="store_true", default=flag_default("manual"), help="Provide laborious manual instructions for obtaining a certificate") helpful.add(["plugins", "certonly", "reconfigure"], "--webroot", action="store_true", default=flag_default("webroot"), help="Obtain certificates by placing files in a webroot directory.") helpful.add(["plugins", "certonly"], "--dns-cloudflare", action="store_true", default=flag_default("dns_cloudflare"), help=("Obtain certificates using a DNS TXT record (if you are " "using Cloudflare for DNS).")) helpful.add(["plugins", "certonly"], "--dns-digitalocean", action="store_true", default=flag_default("dns_digitalocean"), help=("Obtain certificates using a DNS TXT record (if you are " "using DigitalOcean for DNS).")) helpful.add(["plugins", "certonly"], "--dns-dnsimple", action="store_true", default=flag_default("dns_dnsimple"), help=("Obtain certificates using a DNS TXT record (if you are " "using DNSimple for DNS).")) helpful.add(["plugins", "certonly"], "--dns-dnsmadeeasy", action="store_true", default=flag_default("dns_dnsmadeeasy"), help=("Obtain certificates using a DNS TXT record (if you are " "using DNS Made Easy for DNS).")) helpful.add(["plugins", "certonly"], "--dns-gehirn", action="store_true", default=flag_default("dns_gehirn"), help=("Obtain certificates using a DNS TXT record " "(if you are using Gehirn Infrastructure Service for DNS).")) helpful.add(["plugins", "certonly"], "--dns-google", action="store_true", default=flag_default("dns_google"), help=("Obtain certificates using a DNS TXT record (if you are " "using Google Cloud DNS).")) helpful.add(["plugins", "certonly"], "--dns-linode", action="store_true", default=flag_default("dns_linode"), help=("Obtain certificates using a DNS TXT record (if you are " "using Linode for DNS).")) helpful.add(["plugins", "certonly"], "--dns-luadns", action="store_true", default=flag_default("dns_luadns"), help=("Obtain certificates using a DNS TXT record (if you are " "using LuaDNS for DNS).")) helpful.add(["plugins", "certonly"], "--dns-nsone", action="store_true", default=flag_default("dns_nsone"), help=("Obtain certificates using a DNS TXT record (if you are " "using NS1 for DNS).")) helpful.add(["plugins", "certonly"], "--dns-ovh", action="store_true", default=flag_default("dns_ovh"), help=("Obtain certificates using a DNS TXT record (if you are " "using OVH for DNS).")) helpful.add(["plugins", "certonly"], "--dns-rfc2136", action="store_true", default=flag_default("dns_rfc2136"), help="Obtain certificates using a DNS TXT record (if you are using BIND for DNS).") helpful.add(["plugins", "certonly"], "--dns-route53", action="store_true", default=flag_default("dns_route53"), help=("Obtain certificates using a DNS TXT record (if you are using Route53 for " "DNS).")) helpful.add(["plugins", "certonly"], "--dns-sakuracloud", action="store_true", default=flag_default("dns_sakuracloud"), help=("Obtain certificates using a DNS TXT record " "(if you are using Sakura Cloud for DNS).")) # things should not be reorder past/pre this comment: # plugins_group should be displayed in --help before plugin # specific groups (so that plugins_group.description makes sense) helpful.add_plugin_args(plugins) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cli/subparsers.py0000664000175100017510000001010214561227515021111 0ustar00ericaerica"""This module creates subparsers for the argument parser""" from typing import TYPE_CHECKING from certbot import interfaces from certbot._internal import constants from certbot._internal.cli.cli_utils import _EncodeReasonAction from certbot._internal.cli.cli_utils import _user_agent_comment_type from certbot._internal.cli.cli_utils import CaseInsensitiveList from certbot._internal.cli.cli_utils import flag_default from certbot._internal.cli.cli_utils import read_file if TYPE_CHECKING: from certbot._internal.cli import helpful def _create_subparsers(helpful: "helpful.HelpfulArgumentParser") -> None: from certbot._internal.client import sample_user_agent # avoid import loops helpful.add( None, "--user-agent", default=flag_default("user_agent"), help='Set a custom user agent string for the client. User agent strings allow ' 'the CA to collect high level statistics about success rates by OS, ' 'plugin and use case, and to know when to deprecate support for past Python ' "versions and flags. If you wish to hide this information from the Let's " 'Encrypt server, set this to "". ' '(default: {0}). The flags encoded in the user agent are: ' '--duplicate, --force-renew, --allow-subset-of-names, -n, and ' 'whether any hooks are set.'.format(sample_user_agent())) helpful.add( None, "--user-agent-comment", default=flag_default("user_agent_comment"), type=_user_agent_comment_type, help="Add a comment to the default user agent string. May be used when repackaging Certbot " "or calling it from another tool to allow additional statistical data to be collected." " Ignored if --user-agent is set. (Example: Foo-Wrapper/1.0)") helpful.add("certonly", "--csr", default=flag_default("csr"), type=read_file, help="Path to a Certificate Signing Request (CSR) in DER or PEM format." " Currently --csr only works with the 'certonly' subcommand.") helpful.add("revoke", "--reason", dest="reason", choices=CaseInsensitiveList(constants.REVOCATION_REASONS.keys()), action=_EncodeReasonAction, default=flag_default("reason"), help="Specify reason for revoking certificate. (default: unspecified)") helpful.add("revoke", "--delete-after-revoke", action="store_true", default=flag_default("delete_after_revoke"), help="Delete certificates after revoking them, along with all previous and later " "versions of those certificates.") helpful.add("revoke", "--no-delete-after-revoke", action="store_false", dest="delete_after_revoke", default=flag_default("delete_after_revoke"), help="Do not delete certificates after revoking them. This " "option should be used with caution because the 'renew' " "subcommand will attempt to renew undeleted revoked " "certificates.") helpful.add("rollback", "--checkpoints", type=int, metavar="N", default=flag_default("rollback_checkpoints"), help="Revert configuration N number of checkpoints.") helpful.add("plugins", "--init", action="store_true", default=flag_default("init"), help="Initialize plugins.") helpful.add("plugins", "--prepare", action="store_true", default=flag_default("prepare"), help="Initialize and prepare plugins.") helpful.add("plugins", "--authenticators", action="append_const", dest="ifaces", default=flag_default("ifaces"), const=interfaces.Authenticator, help="Limit to authenticator plugins only.") helpful.add("plugins", "--installers", action="append_const", dest="ifaces", default=flag_default("ifaces"), const=interfaces.Installer, help="Limit to installer plugins only.") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/cli/verb_help.py0000664000175100017510000001214714561227515020701 0ustar00ericaerica"""This module contain help information for verbs supported by certbot""" from certbot._internal.cli.cli_constants import SHORT_USAGE # The attributes here are: # short: a string that will be displayed by "certbot -h commands" # opts: a string that heads the section of flags with which this command is documented, # both for "certbot -h SUBCOMMAND" and "certbot -h all" # usage: an optional string that overrides the header of "certbot -h SUBCOMMAND" VERB_HELP = [ ("run (default)", { "short": "Obtain/renew a certificate, and install it", "opts": "Options for obtaining & installing certificates", "usage": SHORT_USAGE.replace("[SUBCOMMAND]", ""), "realname": "run" }), ("certonly", { "short": "Obtain or renew a certificate, but do not install it", "opts": "Options for modifying how a certificate is obtained", "usage": ("\n\n certbot certonly [options] [-d DOMAIN] [-d DOMAIN] ...\n\n" "This command obtains a TLS/SSL certificate without installing it anywhere.") }), ("renew", { "short": "Renew all certificates (or one specified with --cert-name)", "opts": ("The 'renew' subcommand will attempt to renew any certificates" " previously obtained if they are close to expiry, and print a" " summary of the results. By default, 'renew' will reuse the" " plugins and options used to obtain or most recently renew each" " certificate. You can test whether future renewals will succeed" " with `--dry-run`." " Individual certificates can be renewed with the `--cert-name`" " option. Hooks are available to run commands" " before and after renewal; see" " https://certbot.eff.org/docs/using.html#renewal for more" " information on these."), "usage": "\n\n certbot renew [--cert-name CERTNAME] [options]\n\n" }), ("certificates", { "short": "List certificates managed by Certbot", "opts": "List certificates managed by Certbot", "usage": ("\n\n certbot certificates [options] ...\n\n" "Print information about the status of certificates managed by Certbot.") }), ("delete", { "short": "Clean up all files related to a certificate", "opts": "Options for deleting a certificate", "usage": "\n\n certbot delete --cert-name CERTNAME\n\n" }), ("revoke", { "short": "Revoke a certificate specified with --cert-path or --cert-name", "opts": "Options for revocation of certificates", "usage": "\n\n certbot revoke [--cert-path /path/to/fullchain.pem | " "--cert-name example.com] [options]\n\n" }), ("register", { "short": "Register for account with Let's Encrypt / other ACME server", "opts": "Options for account registration", "usage": "\n\n certbot register --email user@example.com [options]\n\n" }), ("update_account", { "short": "Update existing account with Let's Encrypt / other ACME server", "opts": "Options for account modification", "usage": "\n\n certbot update_account --email updated_email@example.com [options]\n\n" }), ("unregister", { "short": "Irrevocably deactivate your account", "opts": "Options for account deactivation.", "usage": "\n\n certbot unregister [options]\n\n" }), ("install", { "short": "Install an arbitrary certificate in a server", "opts": "Options for modifying how a certificate is deployed", "usage": "\n\n certbot install --cert-path /path/to/fullchain.pem " " --key-path /path/to/private-key [options]\n\n" }), ("rollback", { "short": "Roll back server conf changes made during certificate installation", "opts": "Options for rolling back server configuration changes", "usage": "\n\n certbot rollback --checkpoints 3 [options]\n\n" }), ("plugins", { "short": "List plugins that are installed and available on your system", "opts": 'Options for the "plugins" subcommand', "usage": "\n\n certbot plugins [options]\n\n" }), ("enhance", { "short": "Add security enhancements to your existing configuration", "opts": ("Helps to harden the TLS configuration by adding security enhancements " "to already existing configuration."), "usage": "\n\n certbot enhance [options]\n\n" }), ("show_account", { "short": "Show account details from an ACME server", "opts": 'Options useful for the "show_account" subcommand:', "usage": "\n\n certbot show_account [options]\n\n" }), ("reconfigure", { "short": "Update renewal configuration for a certificate specified by --cert-name", "opts": 'Common options that may be updated with the "reconfigure" subcommand:', "usage": "\n\n certbot reconfigure --cert-name CERTNAME [options]\n\n" }), ] # VERB_HELP is a list in order to preserve order, but a dict is sometimes useful VERB_HELP_MAP = dict(VERB_HELP) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/client.py0000664000175100017510000011015714561227515017442 0ustar00ericaerica"""Certbot client API.""" import datetime import logging import platform from typing import Any from typing import Callable from typing import cast from typing import Dict from typing import IO from typing import List from typing import Optional from typing import Tuple from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key import josepy as jose from josepy import ES256 from josepy import ES384 from josepy import ES512 from josepy import RS256 import OpenSSL from acme import client as acme_client from acme import crypto_util as acme_crypto_util from acme import errors as acme_errors from acme import messages import certbot from certbot import configuration from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot import util from certbot._internal import account from certbot._internal import auth_handler from certbot._internal import cli from certbot._internal import constants from certbot._internal import eff from certbot._internal import error_handler from certbot._internal import storage from certbot._internal.plugins import disco as plugin_disco from certbot._internal.plugins import selection as plugin_selection from certbot.compat import os from certbot.display import ops as display_ops from certbot.display import util as display_util from certbot.interfaces import AccountStorage logger = logging.getLogger(__name__) def acme_from_config_key(config: configuration.NamespaceConfig, key: jose.JWK, regr: Optional[messages.RegistrationResource] = None ) -> acme_client.ClientV2: """Wrangle ACME client construction""" if key.typ == 'EC': public_key = key.key if public_key.key_size == 256: alg = ES256 elif public_key.key_size == 384: alg = ES384 elif public_key.key_size == 521: alg = ES512 else: raise errors.NotSupportedError( "No matching signing algorithm can be found for the key" ) else: alg = RS256 net = acme_client.ClientNetwork(key, alg=alg, account=regr, verify_ssl=(not config.no_verify_ssl), user_agent=determine_user_agent(config)) directory = acme_client.ClientV2.get_directory(config.server, net) return acme_client.ClientV2(directory, net) def determine_user_agent(config: configuration.NamespaceConfig) -> str: """ Set a user_agent string in the config based on the choice of plugins. (this wasn't knowable at construction time) :returns: the client's User-Agent string :rtype: `str` """ # WARNING: To ensure changes are in line with Certbot's privacy # policy, talk to a core Certbot team member before making any # changes here. if config.user_agent is None: ua = ("CertbotACMEClient/{0} ({1}; {2}{8}) Authenticator/{3} Installer/{4} " "({5}; flags: {6}) Py/{7}") if os.environ.get("CERTBOT_DOCS") == "1": cli_command = "certbot" os_info = "OS_NAME OS_VERSION" python_version = "major.minor.patchlevel" else: cli_command = cli.cli_command os_info = util.get_os_info_ua() python_version = platform.python_version() ua = ua.format(certbot.__version__, cli_command, os_info, config.authenticator, config.installer, config.verb, ua_flags(config), python_version, "; " + config.user_agent_comment if config.user_agent_comment else "") else: ua = config.user_agent return ua def ua_flags(config: configuration.NamespaceConfig) -> str: """Turn some very important CLI flags into clues in the user agent.""" if isinstance(config, DummyConfig): return "FLAGS" flags = [] if config.duplicate: flags.append("dup") if config.renew_by_default: flags.append("frn") if config.allow_subset_of_names: flags.append("asn") if config.noninteractive_mode: flags.append("n") hook_names = ("pre", "post", "renew", "manual_auth", "manual_cleanup") hooks = [getattr(config, h + "_hook") for h in hook_names] if any(hooks): flags.append("hook") return " ".join(flags) class DummyConfig: """Shim for computing a sample user agent.""" def __init__(self) -> None: self.authenticator = "XXX" self.installer = "YYY" self.user_agent = None self.verb = "SUBCOMMAND" def __getattr__(self, name: str) -> Any: """Any config properties we might have are None.""" return None def sample_user_agent() -> str: """Document what this Certbot's user agent string will be like.""" # DummyConfig is designed to mock certbot.configuration.NamespaceConfig. # Let mypy accept that. return determine_user_agent(cast(configuration.NamespaceConfig, DummyConfig())) def register(config: configuration.NamespaceConfig, account_storage: AccountStorage, tos_cb: Optional[Callable[[str], None]] = None ) -> Tuple[account.Account, acme_client.ClientV2]: """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 a Term of Service URL as a string, and raise an exception if the TOS is not accepted by the client. ``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 perform_registration(acme: acme_client.ClientV2, config: configuration.NamespaceConfig, tos_cb: Optional[Callable[[str], None]]) -> messages.RegistrationResource: """ Actually register new account, trying repeatedly if there are email problems :param acme.client.Client acme: ACME client object. :param certbot.configuration.NamespaceConfig config: Client configuration. :param Callable tos_cb: a callback to handle Term of Service agreement. :returns: Registration Resource. :rtype: `acme.messages.RegistrationResource` """ eab_credentials_supplied = config.eab_kid and config.eab_hmac_key eab: Optional[Dict[str, Any]] if eab_credentials_supplied: account_public_key = acme.net.key.public_key() eab = messages.ExternalAccountBinding.from_data(account_public_key=account_public_key, kid=config.eab_kid, hmac_key=config.eab_hmac_key, directory=acme.directory) else: eab = None if acme.external_account_required(): if not eab_credentials_supplied: msg = ("Server requires external account binding." " Please use --eab-kid and --eab-hmac-key.") raise errors.Error(msg) tos = acme.directory.meta.terms_of_service if tos_cb and tos: tos_cb(tos) try: return acme.new_account(messages.NewRegistration.from_data( email=config.email, terms_of_service_agreed=True, external_account_binding=eab)) except messages.Error as e: if e.code in ("invalidEmail", "invalidContact"): if config.noninteractive_mode: msg = (f"The ACME server believes {config.email} is an invalid email address. " "Please ensure it is a valid email and attempt " "registration again.") raise errors.Error(msg) config.email = display_ops.get_email(invalid=True) return perform_registration(acme, config, tos_cb) raise class Client: """Certbot's client. :ivar certbot.configuration.NamespaceConfig config: Client configuration. :ivar .Account account: Account registered with `register`. :ivar .AuthHandler auth_handler: Authorizations handler that will dispatch DV challenges to appropriate authenticators (providing `.Authenticator` interface). :ivar .Authenticator auth: Prepared (`.Authenticator.prepare`) authenticator that can solve ACME challenges. :ivar .Installer installer: Installer. :ivar acme.client.ClientV2 acme: Optional ACME client API handle. You might already have one from `register`. """ def __init__(self, config: configuration.NamespaceConfig, account_: Optional[account.Account], auth: Optional[interfaces.Authenticator], installer: Optional[interfaces.Installer], acme: Optional[acme_client.ClientV2] = None) -> None: """Initialize a client.""" self.config = config self.account = account_ self.auth = auth self.installer = installer # Initialize ACME if account is provided if acme is None and self.account is not None: acme = acme_from_config_key(config, self.account.key, self.account.regr) self.acme = acme self.auth_handler: Optional[auth_handler.AuthHandler] if auth is not None: self.auth_handler = auth_handler.AuthHandler( auth, self.acme, self.account, self.config.pref_challs) else: self.auth_handler = None def obtain_certificate_from_csr(self, csr: util.CSR, orderr: Optional[messages.OrderResource] = None ) -> Tuple[bytes, bytes]: """Obtain certificate. :param .util.CSR csr: PEM-encoded Certificate Signing Request. The key used to generate this CSR can be different than `authkey`. :param acme.messages.OrderResource orderr: contains authzrs :returns: certificate and chain as PEM byte strings :rtype: tuple """ if self.auth_handler is None: msg = ("Unable to obtain certificate because authenticator is " "not set.") logger.error(msg) raise errors.Error(msg) if self.account is None or self.account.regr is None: raise errors.Error("Please register with the ACME server first.") if self.acme is None: raise errors.Error("ACME client is not set.") logger.debug("CSR: %s", csr) if orderr is None: orderr = self._get_order_and_authorizations(csr.data, best_effort=False) deadline = datetime.datetime.now() + datetime.timedelta( seconds=self.config.issuance_timeout) logger.debug("Will poll for certificate issuance until %s", deadline) orderr = self.acme.finalize_order( orderr, deadline, fetch_alternative_chains=self.config.preferred_chain is not None) fullchain = orderr.fullchain_pem if self.config.preferred_chain and orderr.alternative_fullchains_pem: fullchain = crypto_util.find_chain_with_issuer( [fullchain] + orderr.alternative_fullchains_pem, self.config.preferred_chain, not self.config.dry_run) cert, chain = crypto_util.cert_and_chain_from_fullchain(fullchain) return cert.encode(), chain.encode() def obtain_certificate(self, domains: List[str], old_keypath: Optional[str] = None ) -> Tuple[bytes, bytes, util.Key, util.CSR]: """Obtains a certificate from the ACME server. `.register` must be called before `.obtain_certificate` :param list domains: domains to get a certificate :returns: certificate as PEM string, chain as PEM string, newly generated private key (`.util.Key`), and DER-encoded Certificate Signing Request (`.util.CSR`). :rtype: tuple """ # We need to determine the key path, key PEM data, CSR path, # and CSR PEM data. For a dry run, the paths are None because # they aren't permanently saved to disk. For a lineage with # --reuse-key, the key path and PEM data are derived from an # existing file. if old_keypath is not None: # We've been asked to reuse a specific existing private key. # Therefore, we'll read it now and not generate a new one in # either case below. # # We read in bytes here because the type of `key.pem` # created below is also bytes. with open(old_keypath, "rb") as f: keypath = old_keypath keypem = f.read() key: Optional[util.Key] = util.Key(file=keypath, pem=keypem) logger.info("Reusing existing private key from %s.", old_keypath) else: # The key is set to None here but will be created below. key = None key_size = self.config.rsa_key_size elliptic_curve = "secp256r1" # key-type defaults to a list, but we are only handling 1 currently if isinstance(self.config.key_type, list): self.config.key_type = self.config.key_type[0] if self.config.elliptic_curve and self.config.key_type == 'ecdsa': elliptic_curve = self.config.elliptic_curve self.config.auth_chain_path = "./chain-ecdsa.pem" self.config.auth_cert_path = "./cert-ecdsa.pem" self.config.key_path = "./key-ecdsa.pem" elif self.config.rsa_key_size and self.config.key_type.lower() == 'rsa': key_size = self.config.rsa_key_size # Create CSR from names if self.config.dry_run: key = key or util.Key( file=None, pem=crypto_util.make_key( bits=key_size, elliptic_curve=elliptic_curve, key_type=self.config.key_type, ), ) csr = util.CSR(file=None, form="pem", data=acme_crypto_util.make_csr( key.pem, domains, self.config.must_staple)) else: key = key or crypto_util.generate_key( key_size=key_size, key_dir=None, key_type=self.config.key_type, elliptic_curve=elliptic_curve, strict_permissions=self.config.strict_permissions, ) csr = crypto_util.generate_csr( key, domains, None, self.config.must_staple, self.config.strict_permissions) try: orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names) except messages.Error as error: # Some domains may be rejected during order creation. # Certbot can retry the operation without the rejected # domains contained within subproblems. if self.config.allow_subset_of_names: successful_domains = self._successful_domains_from_error(error, domains) if successful_domains != domains and len(successful_domains) != 0: return self._retry_obtain_certificate(domains, successful_domains) raise authzr = orderr.authorizations auth_domains = {a.body.identifier.value for a in authzr} successful_domains = [d for d in domains if d in auth_domains] # allow_subset_of_names is currently disabled for wildcard # certificates. The reason for this and checking allow_subset_of_names # below is because successful_domains == domains is never true if # domains contains a wildcard because the ACME spec forbids identifiers # in authzs from containing a wildcard character. if self.config.allow_subset_of_names and successful_domains != domains: return self._retry_obtain_certificate(domains, successful_domains) else: try: cert, chain = self.obtain_certificate_from_csr(csr, orderr) return cert, chain, key, csr except messages.Error as error: # Some domains may be rejected during the very late stage of # order finalization. Certbot can retry the operation without # the rejected domains contained within subproblems. if self.config.allow_subset_of_names: successful_domains = self._successful_domains_from_error(error, domains) if successful_domains != domains and len(successful_domains) != 0: return self._retry_obtain_certificate(domains, successful_domains) raise def _get_order_and_authorizations(self, csr_pem: bytes, best_effort: bool) -> messages.OrderResource: """Request a new order and complete its authorizations. :param bytes csr_pem: A CSR in PEM format. :param bool best_effort: True if failing to complete all authorizations should not raise an exception :returns: order resource containing its completed authorizations :rtype: acme.messages.OrderResource """ if not self.acme: raise errors.Error("ACME client is not set.") try: orderr = self.acme.new_order(csr_pem) except acme_errors.WildcardUnsupportedError: raise errors.Error("The currently selected ACME CA endpoint does" " not support issuing wildcard certificates.") if not self.auth_handler: raise errors.Error("No authorization handler has been set.") # For a dry run, ensure we have an order with fresh authorizations if orderr and self.config.dry_run: deactivated, failed = self.auth_handler.deactivate_valid_authorizations(orderr) if deactivated: logger.debug("Recreating order after authz deactivations") orderr = self.acme.new_order(csr_pem) if failed: logger.warning("Certbot was unable to obtain fresh authorizations for every domain" ". The dry run will continue, but results may not be accurate.") authzr = self.auth_handler.handle_authorizations(orderr, self.config, best_effort) return orderr.update(authorizations=authzr) def obtain_and_enroll_certificate(self, domains: List[str], certname: Optional[str] ) -> Optional[storage.RenewableCert]: """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified authenticator and installer, and then create a new renewable lineage containing it. :param domains: domains to request a certificate for :type domains: `list` of `str` :param certname: requested name of lineage :type certname: `str` or `None` :returns: A new :class:`certbot._internal.storage.RenewableCert` instance referred to the enrolled cert lineage, or None if doing a successful dry run. """ new_name = self._choose_lineagename(domains, certname) cert, chain, key, _ = self.obtain_certificate(domains) if (self.config.config_dir != constants.CLI_DEFAULTS["config_dir"] or self.config.work_dir != constants.CLI_DEFAULTS["work_dir"]): logger.info( "Non-standard path(s), might not work with crontab installed " "by your operating system package manager") if self.config.dry_run: logger.debug("Dry run: Skipping creating new lineage for %s", new_name) return None return storage.RenewableCert.new_lineage( new_name, cert, key.pem, chain, self.config) def _successful_domains_from_error(self, error: messages.Error, domains: List[str], ) -> List[str]: if error.subproblems is not None: failed_domains = [problem.identifier.value for problem in error.subproblems if problem.identifier is not None] successful_domains = [x for x in domains if x not in failed_domains] return successful_domains return [] def _retry_obtain_certificate(self, domains: List[str], successful_domains: List[str] ) -> Tuple[bytes, bytes, util.Key, util.CSR]: failed_domains = [d for d in domains if d not in successful_domains] domains_list = ", ".join(failed_domains) display_util.notify("Unable to obtain a certificate with every requested " f"domain. Retrying without: {domains_list}") return self.obtain_certificate(successful_domains) def _choose_lineagename(self, domains: List[str], certname: Optional[str]) -> str: """Chooses a name for the new lineage. :param domains: domains in certificate request :type domains: `list` of `str` :param certname: requested name of lineage :type certname: `str` or `None` :returns: lineage name that should be used :rtype: str :raises errors.Error: If the chosen lineage name is invalid. """ # Remember chosen name for new lineage lineagename = None if certname: lineagename = certname elif util.is_wildcard_domain(domains[0]): # Don't make files and directories starting with *. lineagename = domains[0][2:] else: lineagename = domains[0] # Verify whether chosen lineage is valid if self._is_valid_lineagename(lineagename): return lineagename else: raise errors.Error( "The provided certname cannot be used as a lineage name because it contains " "an illegal character (i.e. filepath separator)." if certname else "Cannot use domain name as lineage name because it contains an illegal " "character (i.e. filepath separator). Specify an explicit lineage name " "with --cert-name.") def _is_valid_lineagename(self, name: str) -> bool: """Determines whether the provided name is a valid lineagename. A lineagename is invalid when it contains filepath separators. :param name: the lineage name to determine validity for :type name: `str` :returns: Whether the provided string constitutes a valid lineage name. :rtype: bool """ return os.path.sep not in name def save_certificate(self, cert_pem: bytes, chain_pem: bytes, cert_path: str, chain_path: str, fullchain_path: str ) -> Tuple[str, str, str]: """Saves the certificate received from the ACME server. :param bytes cert_pem: :param bytes chain_pem: :param str cert_path: Candidate path to a certificate. :param str chain_path: Candidate path to a certificate chain. :param str fullchain_path: Candidate path to a full cert chain. :returns: cert_path, chain_path, and fullchain_path as absolute paths to the actual files :rtype: `tuple` of `str` :raises IOError: If unable to find room to write the cert files """ for path in cert_path, chain_path, fullchain_path: util.make_or_verify_dir(os.path.dirname(path), 0o755, self.config.strict_permissions) cert_file, abs_cert_path = _open_pem_file(self.config, 'cert_path', cert_path) try: cert_file.write(cert_pem) finally: cert_file.close() chain_file, abs_chain_path = _open_pem_file(self.config, 'chain_path', chain_path) fullchain_file, abs_fullchain_path = _open_pem_file( self.config, 'fullchain_path', fullchain_path) _save_chain(chain_pem, chain_file) _save_chain(cert_pem + chain_pem, fullchain_file) return abs_cert_path, abs_chain_path, abs_fullchain_path 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 enhance_config(self, domains: List[str], chain_path: str, redirect_default: bool = True) -> None: """Enhance the configuration. :param list domains: list of domains to configure :param chain_path: chain file path :type chain_path: `str` or `None` :param redirect_default: boolean value that the "redirect" flag should default to :raises .errors.Error: if no installer is specified in the client. """ if self.installer is None: logger.error("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 = redirect_default if config_value: self.apply_enhancement(domains, enhancement_name, option) enhanced = True elif config_value: logger.error( "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 apply_enhancement(self, domains: List[str], enhancement: str, options: Optional[str] = None) -> None: """Applies an enhancement on all domains. :param list domains: list of ssl_vhosts (as strings) :param str enhancement: name of enhancement, e.g. ensure-http-header :param str options: options to enhancement, e.g. Strict-Transport-Security .. note:: When more `options` are needed, make options a list. :raises .errors.PluginError: If Enhancement is not supported, or if there is any other problem with the enhancement. """ if not self.installer: raise errors.Error("No installer plugin has been set.") enh_label = options if enhancement == "ensure-http-header" else enhancement with error_handler.ErrorHandler(self._recovery_routine_with_msg, None): for dom in domains: try: self.installer.enhance(dom, enhancement, options) except errors.PluginEnhancementAlreadyPresent: logger.info("Enhancement %s was already set.", enh_label) except errors.PluginError: logger.error("Unable to set the %s enhancement for %s.", enh_label, dom) raise self.installer.save(f"Add enhancement {enh_label}") def _recovery_routine_with_msg(self, success_msg: Optional[str]) -> None: """Calls the installer's recovery routine and prints success_msg :param str success_msg: message to show on successful recovery """ if self.installer: self.installer.recovery_routine() if success_msg: display_util.notify(success_msg) def _rollback_and_restart(self, success_msg: str) -> None: """Rollback the most recent checkpoint and restart the webserver :param str success_msg: message to show on successful rollback """ if self.installer: logger.info("Rolling back to previous server configuration...") try: self.installer.rollback_checkpoints() self.installer.restart() except: logger.error( "An error occurred and we failed to restore your config and " "restart your server. Please post to " "https://community.letsencrypt.org/c/help " "with details about your configuration and this error you received." ) raise display_util.notify(success_msg) def validate_key_csr(privkey: util.Key, csr: Optional[util.CSR] = None) -> None: """Validate Key and CSR files. Verifies that the client key and csr arguments are valid and correspond to one another. This does not currently check the names in the CSR due to the inability to read SANs from CSRs in python crypto libraries. If csr is left as None, only the key will be validated. :param privkey: Key associated with CSR :type privkey: :class:`certbot.util.Key` :param .util.CSR csr: CSR :raises .errors.Error: when validation fails """ # 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 # Key must be readable and valid. if privkey.pem and not crypto_util.valid_privkey(privkey.pem): raise errors.Error("The provided key is not a valid key") if csr: if csr.form == "der": csr_obj = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, csr.data) cert_buffer = OpenSSL.crypto.dump_certificate_request( OpenSSL.crypto.FILETYPE_PEM, csr_obj ) csr = util.CSR(csr.file, cert_buffer, "pem") # If CSR is provided, it must be readable and valid. if csr.data and not crypto_util.valid_csr(csr.data): raise errors.Error("The provided CSR is not a valid CSR") # If both CSR and key are provided, the key must be the same key used # in the CSR. if csr.data and privkey.pem: if not crypto_util.csr_matches_pubkey( csr.data, privkey.pem): raise errors.Error("The key and CSR do not match") def rollback(default_installer: str, checkpoints: int, config: configuration.NamespaceConfig, plugins: plugin_disco.PluginsRegistry) -> None: """Revert configuration the specified number of checkpoints. :param str default_installer: Default installer name to use for the rollback :param int checkpoints: Number of checkpoints to revert. :param config: Configuration. :type config: :class:`certbot.configuration.NamespaceConfiguration` :param plugins: Plugins available :type plugins: :class:`certbot._internal.plugins.disco.PluginsRegistry` """ # Misconfigurations are only a slight problems... allow the user to rollback installer = plugin_selection.pick_installer( config, default_installer, plugins, question="Which installer " "should be used for rollback?") # No Errors occurred during init... proceed normally # If installer is None... couldn't find an installer... there shouldn't be # anything to rollback if installer is not None: installer.rollback_checkpoints(checkpoints) installer.restart() def _open_pem_file(config: configuration.NamespaceConfig, cli_arg_path: str, pem_path: str) -> Tuple[IO, str]: """Open a pem file. If cli_arg_path was set by the client, open that. Otherwise, uniquify the file path. :param str cli_arg_path: the cli arg name, e.g. cert_path :param str pem_path: the pem file path to open :returns: a tuple of file object and its absolute file path """ if config.set_by_user(cli_arg_path): return util.safe_open(pem_path, chmod=0o644, mode="wb"),\ os.path.abspath(pem_path) uniq = util.unique_file(pem_path, 0o644, "wb") return uniq[0], os.path.abspath(uniq[1]) def _save_chain(chain_pem: bytes, chain_file: IO) -> None: """Saves chain_pem at a unique path based on chain_path. :param bytes chain_pem: certificate chain in PEM format :param str chain_file: chain file object """ try: chain_file.write(chain_pem) finally: chain_file.close() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/constants.py0000664000175100017510000001670014561227515020177 0ustar00ericaerica"""Certbot constants.""" import atexit import logging import sys from contextlib import ExitStack from typing import Any from typing import Dict from acme import challenges from certbot.compat import misc from certbot.compat import os if sys.version_info >= (3, 9): # pragma: no cover import importlib.resources as importlib_resources else: # pragma: no cover import importlib_resources SETUPTOOLS_PLUGINS_ENTRY_POINT = "certbot.plugins" """Setuptools entry point group name for plugins.""" OLD_SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins" """Plugins Setuptools entry point before rename.""" CLI_DEFAULTS: Dict[str, Any] = dict( # pylint: disable=use-dict-literal config_files=[ os.path.join(misc.get_default_folder('config'), 'cli.ini'), # https://freedesktop.org/wiki/Software/xdg-user-dirs/ os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), ], # Main parser verbose_count=0, verbose_level=None, text_mode=False, max_log_backups=1000, preconfigured_renewal=False, noninteractive_mode=False, force_interactive=False, domains=[], certname=None, dry_run=False, register_unsafely_without_email=False, email=None, eff_email=None, reinstall=False, expand=False, renew_by_default=False, renew_with_new_domains=False, autorenew=True, allow_subset_of_names=False, tos=False, account=None, duplicate=False, os_packages_only=False, no_self_upgrade=False, no_permissions_check=False, no_bootstrap=False, quiet=False, staging=False, debug=False, debug_challenges=False, no_verify_ssl=False, http01_port=challenges.HTTP01Response.PORT, http01_address="", https_port=443, break_my_certs=False, rsa_key_size=2048, elliptic_curve="secp256r1", key_type="ecdsa", must_staple=False, redirect=None, auto_hsts=False, hsts=None, uir=None, staple=None, strict_permissions=False, preferred_chain=None, pref_challs=[], validate_hooks=True, directory_hooks=True, reuse_key=False, new_key=False, disable_renew_updates=False, random_sleep_on_renew=True, eab_hmac_key=None, eab_kid=None, issuance_timeout=90, run_deploy_hooks=False, # Subparsers num=None, user_agent=None, user_agent_comment=None, csr=None, reason=0, delete_after_revoke=None, rollback_checkpoints=1, init=False, prepare=False, ifaces=None, # Path parsers auth_cert_path="./cert.pem", auth_chain_path="./chain.pem", key_path=None, config_dir=misc.get_default_folder('config'), work_dir=misc.get_default_folder('work'), logs_dir=misc.get_default_folder('logs'), server="https://acme-v02.api.letsencrypt.org/directory", # Plugins parsers configurator=None, authenticator=None, installer=None, apache=False, nginx=False, standalone=False, manual=False, webroot=False, dns_cloudflare=False, dns_digitalocean=False, dns_dnsimple=False, dns_dnsmadeeasy=False, dns_gehirn=False, dns_google=False, dns_linode=False, dns_luadns=False, dns_nsone=False, dns_ovh=False, dns_rfc2136=False, dns_route53=False, dns_sakuracloud=False ) STAGING_URI = "https://acme-staging-v02.api.letsencrypt.org/directory" V1_URI = "https://acme-v01.api.letsencrypt.org/directory" # The set of reasons for revoking a certificate is defined in RFC 5280 in # section 5.3.1. The reasons that users are allowed to submit are restricted to # those accepted by the ACME server implementation. They are listed in # `letsencrypt.boulder.revocation.reasons.go`. REVOCATION_REASONS = { "unspecified": 0, "keycompromise": 1, "affiliationchanged": 3, "superseded": 4, "cessationofoperation": 5} """Defaults for CLI flags and `certbot.configuration.NamespaceConfig` attributes.""" QUIET_LOGGING_LEVEL = logging.ERROR """Logging level to use in quiet mode.""" DEFAULT_LOGGING_LEVEL = logging.WARNING """Default logging level to use when not in quiet mode.""" RENEWER_DEFAULTS = { "renew_before_expiry": "30 days", } """Defaults for `certbot renew`.""" ARCHIVE_DIR = "archive" """Archive directory, relative to `certbot.configuration.NamespaceConfig.config_dir`.""" CONFIG_DIRS_MODE = 0o755 """Directory mode for ``certbot.configuration.NamespaceConfig.config_dir`` et al.""" ACCOUNTS_DIR = "accounts" """Directory where all accounts are saved.""" LE_REUSE_SERVERS = { os.path.normpath('acme-v02.api.letsencrypt.org/directory'): os.path.normpath('acme-v01.api.letsencrypt.org/directory'), os.path.normpath('acme-staging-v02.api.letsencrypt.org/directory'): os.path.normpath('acme-staging.api.letsencrypt.org/directory') } """Servers that can reuse accounts from other servers.""" BACKUP_DIR = "backups" """Directory (relative to `certbot.configuration.NamespaceConfig.work_dir`) where backups are kept.""" CSR_DIR = "csr" """See `certbot.configuration.NamespaceConfig.csr_dir`.""" IN_PROGRESS_DIR = "IN_PROGRESS" """Directory used before a permanent checkpoint is finalized (relative to `certbot.configuration.NamespaceConfig.work_dir`).""" KEY_DIR = "keys" """Directory (relative to `certbot.configuration.NamespaceConfig.config_dir`) where keys are saved.""" LIVE_DIR = "live" """Live directory, relative to `certbot.configuration.NamespaceConfig.config_dir`.""" TEMP_CHECKPOINT_DIR = "temp_checkpoint" """Temporary checkpoint directory, relative to `certbot.configuration.NamespaceConfig.work_dir`.""" RENEWAL_CONFIGS_DIR = "renewal" """Renewal configs directory, relative to `certbot.configuration.NamespaceConfig.config_dir`.""" RENEWAL_HOOKS_DIR = "renewal-hooks" """Basename of directory containing hooks to run with the renew command.""" RENEWAL_PRE_HOOKS_DIR = "pre" """Basename of directory containing pre-hooks to run with the renew command.""" RENEWAL_DEPLOY_HOOKS_DIR = "deploy" """Basename of directory containing deploy-hooks to run with the renew command.""" RENEWAL_POST_HOOKS_DIR = "post" """Basename of directory containing post-hooks to run with the renew command.""" FORCE_INTERACTIVE_FLAG = "--force-interactive" """Flag to disable TTY checking in certbot.display.util.""" EFF_SUBSCRIBE_URI = "https://supporters.eff.org/subscribe/certbot" """EFF URI used to submit the e-mail address of users who opt-in.""" SSL_DHPARAMS_DEST = "ssl-dhparams.pem" """Name of the ssl_dhparams file as saved in `certbot.configuration.NamespaceConfig.config_dir`.""" def _generate_ssl_dhparams_src_static() -> str: # This code ensures that the resource is accessible as file for the lifetime of current # Python process, and will be automatically cleaned up on exit. file_manager = ExitStack() atexit.register(file_manager.close) ssl_dhparams_src_ref = importlib_resources.files("certbot") / "ssl-dhparams.pem" return str(file_manager.enter_context(importlib_resources.as_file(ssl_dhparams_src_ref))) SSL_DHPARAMS_SRC = _generate_ssl_dhparams_src_static() """Path to the nginx ssl_dhparams file found in the Certbot distribution.""" UPDATED_SSL_DHPARAMS_DIGEST = ".updated-ssl-dhparams-pem-digest.txt" """Name of the hash of the updated or informed ssl_dhparams as saved in `certbot.configuration.NamespaceConfig.config_dir`.""" ALL_SSL_DHPARAMS_HASHES = [ '9ba6429597aeed2d8617a7705b56e96d044f64b07971659382e426675105654b', ] """SHA256 hashes of the contents of all versions of SSL_DHPARAMS_SRC""" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3310835 certbot-2.9.0/certbot/_internal/display/0000775000175100017510000000000014561227516017253 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/display/__init__.py0000664000175100017510000000004114561227515021356 0ustar00ericaerica"""Certbot display utilities.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/display/completer.py0000664000175100017510000000502214561227515021615 0ustar00ericaerica"""Provides Tab completion when prompting users for a path.""" import glob from types import TracebackType from typing import Callable from typing import Iterator from typing import Literal from typing import Optional from typing import Type # readline module is not available on all systems try: import readline except ImportError: import certbot._internal.display.dummy_readline as readline # type: ignore class Completer: """Provides Tab completion when prompting users for a path. This class is meant to be used with readline to provide Tab completion for users entering paths. The complete method can be passed to readline.set_completer directly, however, this function works best as a context manager. For example: with Completer(): raw_input() In this example, Tab completion will be available during the call to raw_input above, however, readline will be restored to its previous state when exiting the body of the with statement. """ def __init__(self) -> None: self._iter: Iterator[str] self._original_completer: Optional[Callable] self._original_delims: str def complete(self, text: str, state: int) -> Optional[str]: """Provides path completion for use with readline. :param str text: text to offer completions for :param int state: which completion to return :returns: possible completion for text or ``None`` if all completions have been returned :rtype: str """ if state == 0: self._iter = glob.iglob(text + '*') return next(self._iter, None) def __enter__(self) -> None: self._original_completer = readline.get_completer() self._original_delims = readline.get_completer_delims() readline.set_completer(self.complete) readline.set_completer_delims(' \t\n;') # readline can be implemented using GNU readline, pyreadline or libedit # which have different configuration syntax if readline.__doc__ is not None and 'libedit' in readline.__doc__: readline.parse_and_bind('bind ^I rl_complete') else: readline.parse_and_bind('tab: complete') def __exit__(self, unused_type: Optional[Type[BaseException]], unused_value: Optional[BaseException], unused_traceback: Optional[TracebackType]) -> 'Literal[False]': readline.set_completer_delims(self._original_delims) readline.set_completer(self._original_completer) return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/display/dummy_readline.py0000664000175100017510000000145314561227515022625 0ustar00ericaerica"""A dummy module with no effect for use on systems without readline.""" from typing import Callable from typing import Iterable from typing import List from typing import Optional def get_completer() -> Optional[Callable[[], str]]: """An empty implementation of readline.get_completer.""" def get_completer_delims() -> List[str]: """An empty implementation of readline.get_completer_delims.""" return [] def parse_and_bind(unused_command: str) -> None: """An empty implementation of readline.parse_and_bind.""" def set_completer(unused_function: Optional[Callable[[], str]] = None) -> None: """An empty implementation of readline.set_completer.""" def set_completer_delims(unused_delims: Iterable[str]) -> None: """An empty implementation of readline.set_completer_delims.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/display/obj.py0000664000175100017510000005370514561227515020410 0ustar00ericaerica"""This modules define the actual display implementations used in Certbot""" import logging import sys from typing import Any from typing import Iterable from typing import List from typing import Optional from typing import TextIO from typing import Tuple from typing import TypeVar from typing import Union from certbot import errors from certbot._internal import constants from certbot._internal.display import completer from certbot._internal.display import util from certbot.compat import os logger = logging.getLogger(__name__) # Display exit codes OK = "ok" """Display exit code indicating user acceptance.""" CANCEL = "cancel" """Display exit code for a user canceling the display.""" # Display constants SIDE_FRAME = ("- " * 39) + "-" """Display boundary (alternates spaces, so when copy-pasted, markdown doesn't interpret it as a heading)""" # This class holds the global state of the display service. Using this class # eliminates potential gotchas that exist if self.display was just a global # variable. In particular, in functions `_DISPLAY = ` would create a # local variable unless the programmer remembered to use the `global` keyword. # Adding a level of indirection causes the lookup of the global _DisplayService # object to happen first avoiding this potential bug. class _DisplayService: def __init__(self) -> None: self.display: Optional[Union[FileDisplay, NoninteractiveDisplay]] = None _SERVICE = _DisplayService() T = TypeVar("T") class FileDisplay: """File-based display.""" # see https://github.com/certbot/certbot/issues/3915 def __init__(self, outfile: TextIO, force_interactive: bool) -> None: super().__init__() self.outfile = outfile self.force_interactive = force_interactive self.skipped_interaction = False def notification(self, message: str, pause: bool = True, wrap: bool = True, force_interactive: bool = False, decorate: bool = True) -> None: """Displays a notification and waits for user acceptance. :param str message: Message to display :param bool pause: Whether or not the program should pause for the user's confirmation :param bool wrap: Whether or not the application should wrap text :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :param bool decorate: Whether to surround the message with a decorated frame """ if wrap: message = util.wrap_lines(message) logger.debug("Notifying user: %s", message) self.outfile.write( (("{line}{frame}{line}" if decorate else "") + "{msg}{line}" + ("{frame}{line}" if decorate else "")) .format(line=os.linesep, frame=SIDE_FRAME, msg=message) ) self.outfile.flush() if pause: if self._can_interact(force_interactive): util.input_with_timeout("Press Enter to Continue") else: logger.debug("Not pausing for user confirmation") def menu(self, message: str, choices: Union[List[Tuple[str, str]], List[str]], ok_label: Optional[str] = None, cancel_label: Optional[str] = None, # pylint: disable=unused-argument help_label: Optional[str] = None, default: Optional[int] = None, # pylint: disable=unused-argument cli_flag: Optional[str] = None, force_interactive: bool = False, **unused_kwargs: Any) -> Tuple[str, int]: """Display a menu. .. todo:: This doesn't enable the help label/button (I wasn't sold on any interface I came up with for this). It would be a nice feature :param str message: title of menu :param choices: Menu lines, len must be > 0 :type choices: list of tuples (tag, item) or list of descriptions (tags will be enumerated) :param default: default value to return (if one exists) :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: tuple of (`code`, `index`) where `code` - str display exit code `index` - int index of the user's selection :rtype: tuple """ return_default = self._return_default(message, default, cli_flag, force_interactive) if return_default is not None: return OK, return_default self._print_menu(message, choices) code, selection = self._get_valid_int_ans(len(choices)) return code, selection - 1 def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, force_interactive: bool = False, **unused_kwargs: Any) -> Tuple[str, str]: """Accept input from the user. :param str message: message to display to the user :param default: default value to return (if one exists) :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: tuple of (`code`, `input`) where `code` - str display exit code `input` - str of the user's input :rtype: tuple """ return_default = self._return_default(message, default, cli_flag, force_interactive) if return_default is not None: return OK, return_default # Trailing space must be added outside of util.wrap_lines to # be preserved message = util.wrap_lines("%s (Enter 'c' to cancel):" % message) + " " ans = util.input_with_timeout(message) if ans in ("c", "C"): return CANCEL, "-1" return OK, ans def yesno(self, message: str, yes_label: str = "Yes", no_label: str = "No", default: Optional[bool] = None, cli_flag: Optional[str] = None, force_interactive: bool = False, **unused_kwargs: Any) -> bool: """Query the user with a yes/no question. Yes and No label must begin with different letters, and must contain at least one letter each. :param str message: question for the user :param str yes_label: Label of the "Yes" parameter :param str no_label: Label of the "No" parameter :param default: default value to return (if one exists) :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: True for "Yes", False for "No" :rtype: bool """ return_default = self._return_default(message, default, cli_flag, force_interactive) if return_default is not None: return return_default message = util.wrap_lines(message) self.outfile.write("{0}{frame}{msg}{0}{frame}".format( os.linesep, frame=SIDE_FRAME + os.linesep, msg=message)) self.outfile.flush() while True: ans = util.input_with_timeout("{yes}/{no}: ".format( yes=util.parens_around_char(yes_label), no=util.parens_around_char(no_label))) # Couldn't get pylint indentation right with elif # elif doesn't matter in this situation if (ans.startswith(yes_label[0].lower()) or ans.startswith(yes_label[0].upper())): return True if (ans.startswith(no_label[0].lower()) or ans.startswith(no_label[0].upper())): return False def checklist(self, message: str, tags: List[str], default: Optional[List[str]] = None, cli_flag: Optional[str] = None, force_interactive: bool = False, **unused_kwargs: Any) -> Tuple[str, List[str]]: """Display a checklist. :param str message: Message to display to user :param list tags: `str` tags to select, len(tags) > 0 :param default: default value to return (if one exists) :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: tuple of (`code`, `tags`) where `code` - str display exit code `tags` - list of selected tags :rtype: tuple """ return_default = self._return_default(message, default, cli_flag, force_interactive) if return_default is not None: return OK, return_default while True: self._print_menu(message, tags) code, ans = self.input("Select the appropriate numbers separated " "by commas and/or spaces, or leave input " "blank to select all options shown", force_interactive=True) if code == OK: if not ans.strip(): ans = " ".join(str(x) for x in range(1, len(tags)+1)) indices = util.separate_list_input(ans) selected_tags = self._scrub_checklist_input(indices, tags) if selected_tags: return code, selected_tags self.outfile.write( "** Error - Invalid selection **%s" % os.linesep) self.outfile.flush() else: return code, [] def _return_default(self, prompt: str, default: Optional[T], cli_flag: Optional[str], force_interactive: bool) -> Optional[T]: """Should we return the default instead of prompting the user? :param str prompt: prompt for the user :param T default: default answer to prompt :param str cli_flag: command line option for setting an answer to this question :param bool force_interactive: if interactivity is forced :returns: The default value if we should return it else `None` :rtype: T or `None` """ # assert_valid_call(prompt, default, cli_flag, force_interactive) if self._can_interact(force_interactive): return None if default is None: msg = "Unable to get an answer for the question:\n{0}".format(prompt) if cli_flag: msg += ( "\nYou can provide an answer on the " "command line with the {0} flag.".format(cli_flag)) raise errors.Error(msg) logger.debug( "Falling back to default %s for the prompt:\n%s", default, prompt) return default def _can_interact(self, force_interactive: bool) -> bool: """Can we safely interact with the user? :param bool force_interactive: if interactivity is forced :returns: True if the display can interact with the user :rtype: bool """ if (self.force_interactive or force_interactive or sys.stdin.isatty() and self.outfile.isatty()): return True if not self.skipped_interaction: logger.warning( "Skipped user interaction because Certbot doesn't appear to " "be running in a terminal. You should probably include " "--non-interactive or %s on the command line.", constants.FORCE_INTERACTIVE_FLAG) self.skipped_interaction = True return False def directory_select(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, force_interactive: bool = False, **unused_kwargs: Any) -> Tuple[str, str]: """Display a directory selection screen. :param str message: prompt to give the user :param default: default value to return (if one exists) :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: tuple of the form (`code`, `string`) where `code` - display exit code `string` - input entered by the user """ with completer.Completer(): return self.input(message, default, cli_flag, force_interactive) def _scrub_checklist_input(self, indices: Iterable[Union[str, int]], tags: List[str]) -> List[str]: """Validate input and transform indices to appropriate tags. :param list indices: input :param list tags: Original tags of the checklist :returns: valid tags the user selected :rtype: :class:`list` of :class:`str` """ # They should all be of type int try: indices_int = [int(index) for index in indices] except ValueError: return [] # Remove duplicates indices_int = list(set(indices_int)) # Check all input is within range for index in indices_int: if index < 1 or index > len(tags): return [] # Transform indices_int to appropriate tags return [tags[index - 1] for index in indices_int] def _print_menu(self, message: str, choices: Union[List[Tuple[str, str]], List[str]]) -> None: """Print a menu on the screen. :param str message: title of menu :param choices: Menu lines :type choices: list of tuples (tag, item) or list of descriptions (tags will be enumerated) """ # Can take either tuples or single items in choices list if choices and isinstance(choices[0], tuple): choices = [f"{c[0]} - {c[1]}" for c in choices] # Write out the message to the user self.outfile.write(f"{os.linesep}{message}{os.linesep}") self.outfile.write(SIDE_FRAME + os.linesep) # Write out the menu choices for i, desc in enumerate(choices, 1): msg = f"{i}: {desc}" self.outfile.write(util.wrap_lines(msg)) # Keep this outside of the textwrap self.outfile.write(os.linesep) self.outfile.write(SIDE_FRAME + os.linesep) self.outfile.flush() def _get_valid_int_ans(self, max_: int) -> Tuple[str, int]: """Get a numerical selection. :param int max: The maximum entry (len of choices), must be positive :returns: tuple of the form (`code`, `selection`) where `code` - str display exit code ('ok' or cancel') `selection` - int user's selection :rtype: tuple """ selection = -1 if max_ > 1: input_msg = ("Select the appropriate number " "[1-{max_}] then [enter] (press 'c' to " "cancel): ".format(max_=max_)) else: input_msg = ("Press 1 [enter] to confirm the selection " "(press 'c' to cancel): ") while selection < 1: ans = util.input_with_timeout(input_msg) if ans.startswith("c") or ans.startswith("C"): return CANCEL, -1 try: selection = int(ans) if selection < 1 or selection > max_: selection = -1 raise ValueError except ValueError: self.outfile.write( "{0}** Invalid input **{0}".format(os.linesep)) self.outfile.flush() return OK, selection class NoninteractiveDisplay: """A display utility implementation that never asks for interactive user input""" def __init__(self, outfile: TextIO, *unused_args: Any, **unused_kwargs: Any) -> None: super().__init__() self.outfile = outfile def _interaction_fail(self, message: str, cli_flag: Optional[str], extra: str = "") -> errors.MissingCommandlineFlag: """Return error to raise in case of an attempt to interact in noninteractive mode""" msg = "Missing command line flag or config entry for this setting:\n" msg += message if extra: msg += "\n" + extra if cli_flag: msg += "\n\n(You can set this with the {0} flag)".format(cli_flag) return errors.MissingCommandlineFlag(msg) def notification(self, message: str, pause: bool = False, wrap: bool = True, # pylint: disable=unused-argument decorate: bool = True, **unused_kwargs: Any) -> None: """Displays a notification without waiting for user acceptance. :param str message: Message to display to stdout :param bool pause: The NoninteractiveDisplay waits for no keyboard :param bool wrap: Whether or not the application should wrap text :param bool decorate: Whether to apply a decorated frame to the message """ if wrap: message = util.wrap_lines(message) logger.debug("Notifying user: %s", message) self.outfile.write( (("{line}{frame}{line}" if decorate else "") + "{msg}{line}" + ("{frame}{line}" if decorate else "")) .format(line=os.linesep, frame=SIDE_FRAME, msg=message) ) self.outfile.flush() def menu(self, message: str, choices: Union[List[Tuple[str, str]], List[str]], ok_label: Optional[str] = None, cancel_label: Optional[str] = None, help_label: Optional[str] = None, default: Optional[int] = None, cli_flag: Optional[str] = None, **unused_kwargs: Any) -> Tuple[str, int]: # pylint: disable=unused-argument """Avoid displaying a menu. :param str message: title of menu :param choices: Menu lines, len must be > 0 :type choices: list of tuples (tag, item) or list of descriptions (tags will be enumerated) :param int default: the default choice :param dict kwargs: absorbs various irrelevant labelling arguments :returns: tuple of (`code`, `index`) where `code` - str display exit code `index` - int index of the user's selection :rtype: tuple :raises errors.MissingCommandlineFlag: if there was no default """ if default is None: raise self._interaction_fail(message, cli_flag, "Choices: " + repr(choices)) return OK, default def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, **unused_kwargs: Any) -> Tuple[str, str]: """Accept input from the user. :param str message: message to display to the user :returns: tuple of (`code`, `input`) where `code` - str display exit code `input` - str of the user's input :rtype: tuple :raises errors.MissingCommandlineFlag: if there was no default """ if default is None: raise self._interaction_fail(message, cli_flag) return OK, default def yesno(self, message: str, yes_label: Optional[str] = None, no_label: Optional[str] = None, # pylint: disable=unused-argument default: Optional[bool] = None, cli_flag: Optional[str] = None, **unused_kwargs: Any) -> bool: """Decide Yes or No, without asking anybody :param str message: question for the user :param dict kwargs: absorbs yes_label, no_label :raises errors.MissingCommandlineFlag: if there was no default :returns: True for "Yes", False for "No" :rtype: bool """ if default is None: raise self._interaction_fail(message, cli_flag) return default def checklist(self, message: str, tags: Iterable[str], default: Optional[List[str]] = None, cli_flag: Optional[str] = None, **unused_kwargs: Any) -> Tuple[str, List[str]]: """Display a checklist. :param str message: Message to display to user :param list tags: `str` tags to select, len(tags) > 0 :param dict kwargs: absorbs default_status arg :returns: tuple of (`code`, `tags`) where `code` - str display exit code `tags` - list of selected tags :rtype: tuple """ if default is None: raise self._interaction_fail(message, cli_flag, "? ".join(tags) + "?") return OK, default def directory_select(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, **unused_kwargs: Any) -> Tuple[str, str]: """Simulate prompting the user for a directory. This function returns default if it is not ``None``, otherwise, an exception is raised explaining the problem. If cli_flag is not ``None``, the error message will include the flag that can be used to set this value with the CLI. :param str message: prompt to give the user :param default: default value to return (if one exists) :param str cli_flag: option used to set this value with the CLI :returns: tuple of the form (`code`, `string`) where `code` - int display exit code `string` - input entered by the user """ return self.input(message, default, cli_flag) def get_display() -> Union[FileDisplay, NoninteractiveDisplay]: """Get the display utility. :return: the display utility :rtype: Union[FileDisplay, NoninteractiveDisplay] :raise: ValueError if the display utility is not configured yet. """ if not _SERVICE.display: raise ValueError("This function was called too early in Certbot's execution " "as the display utility hasn't been configured yet.") return _SERVICE.display def set_display(display: Union[FileDisplay, NoninteractiveDisplay]) -> None: """Set the display service. :param Union[FileDisplay, NoninteractiveDisplay] display: the display service """ _SERVICE.display = display ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/display/util.py0000664000175100017510000000660014561227515020603 0ustar00ericaerica"""Internal Certbot display utilities.""" import sys import textwrap from typing import List from typing import Optional from acme import messages as acme_messages from certbot.compat import misc def wrap_lines(msg: str) -> str: """Format lines nicely to 80 chars. :param str msg: Original message :returns: Formatted message respecting newlines in message :rtype: str """ lines = msg.splitlines() fixed_l = [] for line in lines: fixed_l.append(textwrap.fill( line, 80, break_long_words=False, break_on_hyphens=False)) return '\n'.join(fixed_l) def parens_around_char(label: str) -> str: """Place parens around first character of label. :param str label: Must contain at least one character """ return "({first}){rest}".format(first=label[0], rest=label[1:]) def input_with_timeout(prompt: Optional[str] = None, timeout: float = 36000.0) -> str: """Get user input with a timeout. Behaves the same as the builtin input, however, an error is raised if a user doesn't answer after timeout seconds. The default timeout value was chosen to place it just under 12 hours for users following our advice and running Certbot twice a day. :param str prompt: prompt to provide for input :param float timeout: maximum number of seconds to wait for input :returns: user response :rtype: str :raises errors.Error if no answer is given before the timeout """ # use of sys.stdin and sys.stdout to mimic the builtin input based on # https://github.com/python/cpython/blob/baf7bb30a02aabde260143136bdf5b3738a1d409/Lib/getpass.py#L129 if prompt: sys.stdout.write(prompt) sys.stdout.flush() line = misc.readline_with_timeout(timeout, prompt) if not line: raise EOFError return line.rstrip('\n') def separate_list_input(input_: str) -> List[str]: """Separate a comma or space separated list. :param str input_: input from the user :returns: strings :rtype: list """ no_commas = input_.replace(",", " ") # Each string is naturally unicode, this causes problems with M2Crypto SANs # TODO: check if above is still true when M2Crypto is gone ^ return [str(string) for string in no_commas.split()] def summarize_domain_list(domains: List[str]) -> str: """Summarizes a list of domains in the format of: example.com.com and N more domains or if there is are only two domains: example.com and www.example.com or if there is only one domain: example.com :param list domains: `str` list of domains :returns: the domain list summary :rtype: str """ if not domains: return "" length = len(domains) if length == 1: return domains[0] elif length == 2: return " and ".join(domains) else: return "{0} and {1} more domains".format(domains[0], length-1) def describe_acme_error(error: acme_messages.Error) -> str: """Returns a human-readable description of an RFC7807 error. :param error: The ACME error :returns: a string describing the error, suitable for human consumption. :rtype: str """ parts = (error.title, error.detail) if any(parts): return ' :: '.join(part for part in parts if part is not None) if error.description: return error.description return error.typ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/eff.py0000664000175100017510000001060414561227515016720 0ustar00ericaerica"""Subscribes users to the EFF newsletter.""" import logging from typing import Optional import requests from certbot import configuration from certbot._internal import constants from certbot._internal.account import Account from certbot._internal.account import AccountFileStorage from certbot.display import util as display_util logger = logging.getLogger(__name__) def prepare_subscription(config: configuration.NamespaceConfig, acc: Account) -> None: """High level function to store potential EFF newsletter subscriptions. The user may be asked if they want to sign up for the newsletter if they have not given their explicit approval or refusal using --eff-mail or --no-eff-mail flag. Decision about EFF subscription will be stored in the account metadata. :param configuration.NamespaceConfig config: Client configuration. :param Account acc: Current client account. """ if config.eff_email is False: return if config.eff_email is True: if config.email is None: _report_failure("you didn't provide an e-mail address") else: acc.meta = acc.meta.update(register_to_eff=config.email) elif config.email and _want_subscription(): acc.meta = acc.meta.update(register_to_eff=config.email) if acc.meta.register_to_eff: storage = AccountFileStorage(config) storage.update_meta(acc) def handle_subscription(config: configuration.NamespaceConfig, acc: Optional[Account]) -> None: """High level function to take care of EFF newsletter subscriptions. Once subscription is handled, it will not be handled again. :param configuration.NamespaceConfig config: Client configuration. :param Account acc: Current client account. """ if config.dry_run or not acc: return if acc.meta.register_to_eff: subscribe(acc.meta.register_to_eff) acc.meta = acc.meta.update(register_to_eff=None) storage = AccountFileStorage(config) storage.update_meta(acc) def _want_subscription() -> bool: """Does the user want to be subscribed to the EFF newsletter? :returns: True if we should subscribe the user, otherwise, False :rtype: bool """ prompt = ( 'Would you be willing, once your first certificate is successfully issued, ' 'to share your email address with the Electronic Frontier Foundation, a ' "founding partner of the Let's Encrypt project and the non-profit organization " "that develops Certbot? We'd like to send you email about our work encrypting " "the web, EFF news, campaigns, and ways to support digital freedom. ") return display_util.yesno(prompt, default=False) def subscribe(email: str) -> None: """Subscribe the user to the EFF mailing list. :param str email: the e-mail address to subscribe """ url = constants.EFF_SUBSCRIBE_URI data = {'data_type': 'json', 'email': email, 'form_id': 'eff_supporters_library_subscribe_form'} logger.info('Subscribe to the EFF mailing list (email: %s).', email) logger.debug('Sending POST request to %s:\n%s', url, data) _check_response(requests.post(url, data=data, timeout=60)) def _check_response(response: requests.Response) -> None: """Check for errors in the server's response. If an error occurred, it will be reported to the user. :param requests.Response response: the server's response to the subscription request """ logger.debug('Received response:\n%s', response.content) try: response.raise_for_status() if not response.json()['status']: _report_failure('your e-mail address appears to be invalid') except requests.exceptions.HTTPError: _report_failure() except (ValueError, KeyError): _report_failure('there was a problem with the server response') def _report_failure(reason: Optional[str] = None) -> None: """Notify the user of failing to sign them up for the newsletter. :param reason: a phrase describing what the problem was beginning with a lowercase letter and no closing punctuation :type reason: `str` or `None` """ msg = ['We were unable to subscribe you the EFF mailing list'] if reason is not None: msg.append(' because ') msg.append(reason) msg.append('. You can try again later by visiting https://act.eff.org.') display_util.notify(''.join(msg)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/error_handler.py0000664000175100017510000001643414561227515021015 0ustar00ericaerica"""Registers functions to be called if an exception or signal occurs.""" import functools import logging import signal import traceback from types import TracebackType from typing import Any from typing import Callable from typing import Dict from typing import List from typing import Optional from typing import Type from typing import Union from certbot import errors from certbot.compat import os logger = logging.getLogger(__name__) # _SIGNALS stores the signals that will be handled by the ErrorHandler. These # signals were chosen as their default handler terminates the process and could # potentially occur from inside Python. Signals such as SIGILL were not # included as they could be a sign of something devious and we should terminate # immediately. if os.name != "nt": _SIGNALS = [signal.SIGTERM] for signal_code in [signal.SIGHUP, signal.SIGQUIT, signal.SIGXCPU, signal.SIGXFSZ]: # Adding only those signals that their default action is not Ignore. # This is platform-dependent, so we check it dynamically. if signal.getsignal(signal_code) != signal.SIG_IGN: _SIGNALS.append(signal_code) else: # POSIX signals are not implemented natively in Windows, but emulated from the C runtime. # As consumed by CPython, most of handlers on theses signals are useless, in particular # SIGTERM: for instance, os.kill(pid, signal.SIGTERM) will call TerminateProcess, that stops # immediately the process without calling the attached handler. Besides, non-POSIX signals # (CTRL_C_EVENT and CTRL_BREAK_EVENT) are implemented in a console context to handle the # CTRL+C event to a process launched from the console. Only CTRL_C_EVENT has a reliable # behavior in fact, and maps to the handler to SIGINT. However in this case, a # KeyboardInterrupt is raised, that will be handled by ErrorHandler through the context manager # protocol. Finally, no signal on Windows is electable to be handled using ErrorHandler. # # Refs: https://stackoverflow.com/a/35792192, https://maruel.ca/post/python_windows_signal, # https://docs.python.org/2/library/os.html#os.kill, # https://www.reddit.com/r/Python/comments/1dsblt/windows_command_line_automation_ctrlc_question _SIGNALS = [] class ErrorHandler: """Context manager for running code that must be cleaned up on failure. The context manager allows you to register functions that will be called when an exception (excluding SystemExit) or signal is encountered. Usage:: handler = ErrorHandler(cleanup1_func, *cleanup1_args, **cleanup1_kwargs) handler.register(cleanup2_func, *cleanup2_args, **cleanup2_kwargs) with handler: do_something() Or for one cleanup function:: with ErrorHandler(func, args, kwargs): do_something() If an exception is raised out of do_something, the cleanup functions will be called in last in first out order. Then the exception is raised. Similarly, if a signal is encountered, the cleanup functions are called followed by the previously received signal handler. Each registered cleanup function is called exactly once. If a registered function raises an exception, it is logged and the next function is called. Signals received while the registered functions are executing are deferred until they finish. """ def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None: self.call_on_regular_exit = False self.body_executed = False self.funcs: List[Callable[[], Any]] = [] self.prev_handlers: Dict[int, Union[int, None, Callable]] = {} self.received_signals: List[int] = [] if func is not None: self.register(func, *args, **kwargs) def __enter__(self) -> None: self.body_executed = False self._set_signal_handlers() def __exit__(self, exec_type: Optional[Type[BaseException]], exec_value: Optional[BaseException], trace: Optional[TracebackType]) -> bool: self.body_executed = True retval = False # SystemExit is ignored to properly handle forks that don't exec if exec_type is SystemExit: return retval if exec_type is None: if not self.call_on_regular_exit: return retval elif exec_type is errors.SignalExit: logger.debug("Encountered signals: %s", self.received_signals) retval = True else: logger.debug("Encountered exception:\n%s", "".join( traceback.format_exception(exec_type, exec_value, trace))) self._call_registered() self._reset_signal_handlers() self._call_signals() return retval def register(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None: """Sets func to be run with the given arguments during cleanup. :param function func: function to be called in case of an error """ self.funcs.append(functools.partial(func, *args, **kwargs)) def _call_registered(self) -> None: """Calls all registered functions""" logger.debug("Calling registered functions") while self.funcs: try: self.funcs[-1]() except Exception as exc: # pylint: disable=broad-except output = traceback.format_exception_only(type(exc), exc) logger.error("Encountered exception during recovery: %s", ''.join(output).rstrip()) self.funcs.pop() def _set_signal_handlers(self) -> None: """Sets signal handlers for signals in _SIGNALS.""" for signum in _SIGNALS: prev_handler = signal.getsignal(signum) # If prev_handler is None, the handler was set outside of Python if prev_handler is not None: self.prev_handlers[signum] = prev_handler signal.signal(signum, self._signal_handler) def _reset_signal_handlers(self) -> None: """Resets signal handlers for signals in _SIGNALS.""" for signum, handler in self.prev_handlers.items(): signal.signal(signum, handler) self.prev_handlers.clear() def _signal_handler(self, signum: int, unused_frame: Any) -> None: """Replacement function for handling received signals. Store the received signal. If we are executing the code block in the body of the context manager, stop by raising signal exit. :param int signum: number of current signal """ self.received_signals.append(signum) if not self.body_executed: raise errors.SignalExit def _call_signals(self) -> None: """Finally call the deferred signals.""" for signum in self.received_signals: logger.debug("Calling signal %s", signum) os.kill(os.getpid(), signum) class ExitHandler(ErrorHandler): """Context manager for running code that must be cleaned up. Subclass of ErrorHandler, with the same usage and parameters. In addition to cleaning up on all signals, also cleans up on regular exit. """ def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None: super().__init__(func, *args, **kwargs) self.call_on_regular_exit = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/hooks.py0000664000175100017510000002462414561227515017312 0ustar00ericaerica"""Facilities for implementing hooks that call shell commands.""" import logging from typing import Dict from typing import List from typing import Optional from typing import Set from certbot import configuration from certbot import errors from certbot import util from certbot.compat import filesystem from certbot.compat import misc from certbot.compat import os from certbot.display import ops as display_ops from certbot.plugins import util as plug_util logger = logging.getLogger(__name__) def validate_hooks(config: configuration.NamespaceConfig) -> None: """Check hook commands are executable.""" validate_hook(config.pre_hook, "pre") validate_hook(config.post_hook, "post") validate_hook(config.deploy_hook, "deploy") validate_hook(config.renew_hook, "renew") def _prog(shell_cmd: str) -> Optional[str]: """Extract the program run by a shell command. :param str shell_cmd: command to be executed :returns: basename of command or None if the command isn't found :rtype: str or None """ if not util.exe_exists(shell_cmd): plug_util.path_surgery(shell_cmd) if not util.exe_exists(shell_cmd): return None return os.path.basename(shell_cmd) def validate_hook(shell_cmd: str, hook_name: str) -> None: """Check that a command provided as a hook is plausibly executable. :raises .errors.HookCommandNotFound: if the command is not found """ if shell_cmd: cmd = shell_cmd.split(None, 1)[0] if not _prog(cmd): path = os.environ["PATH"] if os.path.exists(cmd): msg = f"{cmd}-hook command {hook_name} exists, but is not executable." else: msg = ( f"Unable to find {hook_name}-hook command {cmd} in the PATH.\n(PATH is " f"{path})\nSee also the --disable-hook-validation option." ) raise errors.HookCommandNotFound(msg) def pre_hook(config: configuration.NamespaceConfig) -> None: """Run pre-hooks if they exist and haven't already been run. When Certbot is running with the renew subcommand, this function runs any hooks found in the config.renewal_pre_hooks_dir (if they have not already been run) followed by any pre-hook in the config. If hooks in config.renewal_pre_hooks_dir are run and the pre-hook in the config is a path to one of these scripts, it is not run twice. :param configuration.NamespaceConfig config: Certbot settings """ if config.verb == "renew" and config.directory_hooks: for hook in list_hooks(config.renewal_pre_hooks_dir): _run_pre_hook_if_necessary(hook) cmd = config.pre_hook if cmd: _run_pre_hook_if_necessary(cmd) executed_pre_hooks: Set[str] = set() def _run_pre_hook_if_necessary(command: str) -> None: """Run the specified pre-hook if we haven't already. If we've already run this exact command before, a message is logged saying the pre-hook was skipped. :param str command: pre-hook to be run """ if command in executed_pre_hooks: logger.info("Pre-hook command already run, skipping: %s", command) else: _run_hook("pre-hook", command) executed_pre_hooks.add(command) def post_hook( config: configuration.NamespaceConfig, renewed_domains: List[str] ) -> None: """Run post-hooks if defined. This function also registers any executables found in config.renewal_post_hooks_dir to be run when Certbot is used with the renew subcommand. If the verb is renew, we delay executing any post-hooks until :func:`run_saved_post_hooks` is called. In this case, this function registers all hooks found in config.renewal_post_hooks_dir to be called followed by any post-hook in the config. If the post-hook in the config is a path to an executable in the post-hook directory, it is not scheduled to be run twice. :param configuration.NamespaceConfig config: Certbot settings """ cmd = config.post_hook # In the "renew" case, we save these up to run at the end if config.verb == "renew": if config.directory_hooks: for hook in list_hooks(config.renewal_post_hooks_dir): _run_eventually(hook) if cmd: _run_eventually(cmd) # certonly / run elif cmd: renewed_domains_str = ' '.join(renewed_domains) # 32k is reasonable on Windows and likely quite conservative on other platforms if len(renewed_domains_str) > 32_000: logger.warning("Limiting RENEWED_DOMAINS environment variable to 32k characters") renewed_domains_str = renewed_domains_str[:32_000] _run_hook( "post-hook", cmd, { 'RENEWED_DOMAINS': renewed_domains_str, # Since other commands stop certbot execution on failure, # it doesn't make sense to have a FAILED_DOMAINS variable 'FAILED_DOMAINS': "" } ) post_hooks: List[str] = [] def _run_eventually(command: str) -> None: """Registers a post-hook to be run eventually. All commands given to this function will be run exactly once in the order they were given when :func:`run_saved_post_hooks` is called. :param str command: post-hook to register to be run """ if command not in post_hooks: post_hooks.append(command) def run_saved_post_hooks(renewed_domains: List[str], failed_domains: List[str]) -> None: """Run any post hooks that were saved up in the course of the 'renew' verb""" renewed_domains_str = ' '.join(renewed_domains) failed_domains_str = ' '.join(failed_domains) # 32k combined is reasonable on Windows and likely quite conservative on other platforms if len(renewed_domains_str) > 16_000: logger.warning("Limiting RENEWED_DOMAINS environment variable to 16k characters") renewed_domains_str = renewed_domains_str[:16_000] if len(failed_domains_str) > 16_000: logger.warning("Limiting FAILED_DOMAINS environment variable to 16k characters") renewed_domains_str = failed_domains_str[:16_000] for cmd in post_hooks: _run_hook( "post-hook", cmd, { 'RENEWED_DOMAINS': renewed_domains_str, 'FAILED_DOMAINS': failed_domains_str } ) def deploy_hook(config: configuration.NamespaceConfig, domains: List[str], lineage_path: str) -> None: """Run post-issuance hook if defined. :param configuration.NamespaceConfig config: Certbot settings :param domains: domains in the obtained certificate :type domains: `list` of `str` :param str lineage_path: live directory path for the new cert """ if config.deploy_hook: _run_deploy_hook(config.deploy_hook, domains, lineage_path, config.dry_run, config.run_deploy_hooks) def renew_hook(config: configuration.NamespaceConfig, domains: List[str], lineage_path: str) -> None: """Run post-renewal hooks. This function runs any hooks found in config.renewal_deploy_hooks_dir followed by any renew-hook in the config. If the renew-hook in the config is a path to a script in config.renewal_deploy_hooks_dir, it is not run twice. If Certbot is doing a dry run, no hooks are run and messages are logged saying that they were skipped. :param configuration.NamespaceConfig config: Certbot settings :param domains: domains in the obtained certificate :type domains: `list` of `str` :param str lineage_path: live directory path for the new cert """ executed_dir_hooks = set() if config.directory_hooks: for hook in list_hooks(config.renewal_deploy_hooks_dir): _run_deploy_hook(hook, domains, lineage_path, config.dry_run, config.run_deploy_hooks) executed_dir_hooks.add(hook) if config.renew_hook: if config.renew_hook in executed_dir_hooks: logger.info("Skipping deploy-hook '%s' as it was already run.", config.renew_hook) else: _run_deploy_hook(config.renew_hook, domains, lineage_path, config.dry_run, config.run_deploy_hooks) def _run_deploy_hook(command: str, domains: List[str], lineage_path: str, dry_run: bool, run_deploy_hooks: bool) -> None: """Run the specified deploy-hook (if not doing a dry run). If dry_run is True, command is not run and a message is logged saying that it was skipped. If dry_run is False, the hook is run after setting the appropriate environment variables. :param str command: command to run as a deploy-hook :param domains: domains in the obtained certificate :type domains: `list` of `str` :param str lineage_path: live directory path for the new cert :param bool dry_run: True iff Certbot is doing a dry run :param bool run_deploy_hooks: True if deploy hooks should run despite Certbot doing a dry run """ if dry_run and not run_deploy_hooks: logger.info("Dry run: skipping deploy hook command: %s", command) return os.environ["RENEWED_DOMAINS"] = " ".join(domains) os.environ["RENEWED_LINEAGE"] = lineage_path _run_hook("deploy-hook", command) def _run_hook(cmd_name: str, shell_cmd: str, extra_env: Optional[Dict[str, str]] = None) -> str: """Run a hook command. :param str cmd_name: the user facing name of the hook being run :param shell_cmd: shell command to execute :type shell_cmd: `list` of `str` or `str` :param dict extra_env: extra environment variables to set :type extra_env: `dict` of `str` to `str` :returns: stderr if there was any""" env = util.env_no_snap_for_external_calls() env.update(extra_env or {}) returncode, err, out = misc.execute_command_status( cmd_name, shell_cmd, env=env) display_ops.report_executed_command(f"Hook '{cmd_name}'", returncode, out, err) return err def list_hooks(dir_path: str) -> List[str]: """List paths to all hooks found in dir_path in sorted order. :param str dir_path: directory to search :returns: `list` of `str` :rtype: sorted list of absolute paths to executables in dir_path """ allpaths = (os.path.join(dir_path, f) for f in os.listdir(dir_path)) hooks = [path for path in allpaths if filesystem.is_executable(path) and not path.endswith('~')] return sorted(hooks) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/lock.py0000664000175100017510000002376214561227515017121 0ustar00ericaerica"""Implements file locks compatible with Linux and Windows for locking files and directories.""" import errno import logging from typing import Optional from certbot import errors from certbot.compat import filesystem from certbot.compat import os try: import fcntl except ImportError: import msvcrt POSIX_MODE = False else: POSIX_MODE = True logger = logging.getLogger(__name__) class LockFile: """ Platform independent file lock system. LockFile accepts a parameter, the path to a file acting as a lock. Once the LockFile, instance is created, the associated file is 'locked from the point of view of the OS, meaning that if another instance of Certbot try at the same time to acquire the same lock, it will raise an Exception. Calling release method will release the lock, and make it available to every other instance. Upon exit, Certbot will also release all the locks. This allows us to protect a file or directory from being concurrently accessed or modified by two Certbot instances. LockFile is platform independent: it will proceed to the appropriate OS lock mechanism depending on Linux or Windows. """ def __init__(self, path: str) -> None: """ Create a LockFile instance on the given file path, and acquire lock. :param str path: the path to the file that will hold a lock """ self._path = path mechanism = _UnixLockMechanism if POSIX_MODE else _WindowsLockMechanism self._lock_mechanism = mechanism(path) self.acquire() def __repr__(self) -> str: repr_str = '{0}({1}) <'.format(self.__class__.__name__, self._path) if self.is_locked(): repr_str += 'acquired>' else: repr_str += 'released>' return repr_str def acquire(self) -> None: """ Acquire the lock on the file, forbidding any other Certbot instance to acquire it. :raises errors.LockError: if unable to acquire the lock """ self._lock_mechanism.acquire() def release(self) -> None: """ Release the lock on the file, allowing any other Certbot instance to acquire it. """ self._lock_mechanism.release() def is_locked(self) -> bool: """ Check if the file is currently locked. :return: True if the file is locked, False otherwise """ return self._lock_mechanism.is_locked() class _BaseLockMechanism: def __init__(self, path: str) -> None: """ Create a lock file mechanism for Unix. :param str path: the path to the lock file """ self._path = path self._fd: Optional[int] = None def is_locked(self) -> bool: """Check if lock file is currently locked. :return: True if the lock file is locked :rtype: bool """ return self._fd is not None def acquire(self) -> None: # pylint: disable=missing-function-docstring pass # pragma: no cover def release(self) -> None: # pylint: disable=missing-function-docstring pass # pragma: no cover class _UnixLockMechanism(_BaseLockMechanism): """ A UNIX lock file mechanism. This lock file is released when the locked file is closed or the process exits. It cannot be used to provide synchronization between threads. It is based on the lock_file package by Martin Horcicka. """ def acquire(self) -> None: """Acquire the lock.""" while self._fd is None: # Open the file fd = filesystem.open(self._path, os.O_CREAT | os.O_WRONLY, 0o600) try: self._try_lock(fd) if self._lock_success(fd): self._fd = fd finally: # Close the file if it is not the required one if self._fd is None: os.close(fd) def _try_lock(self, fd: int) -> None: """ Try to acquire the lock file without blocking. :param int fd: file descriptor of the opened file to lock """ try: fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except IOError as err: if err.errno in (errno.EACCES, errno.EAGAIN): logger.debug('A lock on %s is held by another process.', self._path) raise errors.LockError('Another instance of Certbot is already running.') raise def _lock_success(self, fd: int) -> bool: """ Did we successfully grab the lock? Because this class deletes the locked file when the lock is released, it is possible another process removed and recreated the file between us opening the file and acquiring the lock. :param int fd: file descriptor of the opened file to lock :returns: True if the lock was successfully acquired :rtype: bool """ # Normally os module should not be imported in certbot codebase except in certbot.compat # for the sake of compatibility over Windows and Linux. # We make an exception here, since _lock_success is private and called only on Linux. from os import fstat # pylint: disable=os-module-forbidden from os import stat # pylint: disable=os-module-forbidden try: stat1 = stat(self._path) except OSError as err: if err.errno == errno.ENOENT: return False raise stat2 = fstat(fd) # If our locked file descriptor and the file on disk refer to # the same device and inode, they're the same file. return stat1.st_dev == stat2.st_dev and stat1.st_ino == stat2.st_ino def release(self) -> None: """Remove, close, and release the lock file.""" # It is important the lock file is removed before it's released, # otherwise: # # process A: open lock file # process B: release lock file # process A: lock file # process A: check device and inode # process B: delete file # process C: open and lock a different file at the same path try: os.remove(self._path) finally: # Following check is done to make mypy happy: it ensure that self._fd, marked # as Optional[int] is effectively int to make it compatible with os.close signature. if self._fd is None: # pragma: no cover raise TypeError('Error, self._fd is None.') try: os.close(self._fd) finally: self._fd = None class _WindowsLockMechanism(_BaseLockMechanism): """ A Windows lock file mechanism. By default on Windows, acquiring a file handler gives exclusive access to the process and results in an effective lock. However, it is possible to explicitly acquire the file handler in shared access in terms of read and write, and this is done by os.open and io.open in Python. So an explicit lock needs to be done through the call of msvcrt.locking, that will lock the first byte of the file. In theory, it is also possible to access a file in shared delete access, allowing other processes to delete an opened file. But this needs also to be done explicitly by all processes using the Windows low level APIs, and Python does not do it. As of Python 3.7 and below, Python developers state that deleting a file opened by a process from another process is not possible with os.open and io.open. Consequently, msvcrt.locking is sufficient to obtain an effective lock, and the race condition encountered on Linux is not possible on Windows, leading to a simpler workflow. """ def acquire(self) -> None: """Acquire the lock""" open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC fd = None try: # Under Windows, filesystem.open will raise directly an EACCES error # if the lock file is already locked. fd = filesystem.open(self._path, open_mode, 0o600) # This "type: ignore" is currently needed because msvcrt methods # are only defined on Windows. See # https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stdlib/msvcrt.pyi. msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) # type: ignore # pylint: disable=used-before-assignment except (IOError, OSError) as err: if fd: os.close(fd) # Anything except EACCES is unexpected. Raise directly the error in that case. if err.errno != errno.EACCES: raise logger.debug('A lock on %s is held by another process.', self._path) raise errors.LockError('Another instance of Certbot is already running.') self._fd = fd def release(self) -> None: """Release the lock.""" try: if not self._fd: raise errors.Error("The lock has not been acquired first.") # This "type: ignore" is currently needed because msvcrt methods # are only defined on Windows. See # https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stdlib/msvcrt.pyi. msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1) # type: ignore # pylint: disable=used-before-assignment os.close(self._fd) try: os.remove(self._path) except OSError as e: # If the lock file cannot be removed, it is not a big deal. # Likely another instance is acquiring the lock we just released. logger.debug(str(e)) finally: self._fd = None def lock_dir(dir_path: str) -> LockFile: """Place a lock file on the directory at dir_path. The lock file is placed in the root of dir_path with the name .certbot.lock. :param str dir_path: path to directory :returns: the locked LockFile object :rtype: LockFile :raises errors.LockError: if unable to acquire the lock """ return LockFile(os.path.join(dir_path, '.certbot.lock')) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/log.py0000664000175100017510000003603014561227515016742 0ustar00ericaerica"""Logging utilities for Certbot. The best way to use this module is through `pre_arg_parse_setup` and `post_arg_parse_setup`. `pre_arg_parse_setup` configures a minimal terminal logger and ensures a detailed log is written to a secure temporary file if Certbot exits before `post_arg_parse_setup` is called. `post_arg_parse_setup` relies on the parsed command line arguments and does the full logging setup with terminal and rotating file handling as configured by the user. Any logged messages before `post_arg_parse_setup` is called are sent to the rotating file handler. Special care is taken by both methods to ensure all errors are logged and properly flushed before program exit. The `logging` module is useful for recording messages about about what Certbot is doing under the hood, but do not necessarily need to be shown to the user on the terminal. The default verbosity is WARNING. The preferred method to display important information to the user is to use `certbot.display.util` and `certbot.display.ops`. """ import functools import logging import logging.handlers import shutil import sys import tempfile import traceback from types import TracebackType from typing import Any from typing import cast from typing import IO from typing import Optional from typing import Tuple from typing import Type from acme import messages from certbot import configuration from certbot import errors from certbot import util from certbot._internal import constants from certbot._internal.display import util as display_util from certbot.compat import os # Logging format CLI_FMT = "%(message)s" FILE_FMT = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" logger = logging.getLogger(__name__) def pre_arg_parse_setup() -> None: """Setup logging before command line arguments are parsed. Terminal logging is setup using `certbot._internal.constants.QUIET_LOGGING_LEVEL` so Certbot is as quiet as possible. File logging is setup so that logging messages are buffered in memory. If Certbot exits before `post_arg_parse_setup` is called, these buffered messages are written to a temporary file. If Certbot doesn't exit, `post_arg_parse_setup` writes the messages to the normal log files. This function also sets `logging.shutdown` to be called on program exit which automatically flushes logging handlers and `sys.excepthook` to properly log/display fatal exceptions. """ temp_handler = TempHandler() temp_handler.setFormatter(logging.Formatter(FILE_FMT)) temp_handler.setLevel(logging.DEBUG) memory_handler = MemoryHandler(temp_handler) stream_handler = ColoredStreamHandler() stream_handler.setFormatter(logging.Formatter(CLI_FMT)) # The pre-argparse logging level is set to WARNING here. This is to ensure that # deprecated flags (see DeprecatedArgumentAction) print something to the terminal. # See https://github.com/certbot/certbot/issues/9618. stream_handler.setLevel(logging.WARNING) root_logger = logging.getLogger() root_logger.setLevel(logging.DEBUG) # send all records to handlers root_logger.addHandler(memory_handler) root_logger.addHandler(stream_handler) # logging.shutdown will flush the memory handler because flush() and # close() are explicitly called util.atexit_register(logging.shutdown) sys.excepthook = functools.partial( pre_arg_parse_except_hook, memory_handler, debug='--debug' in sys.argv, quiet='--quiet' in sys.argv or '-q' in sys.argv, log_path=temp_handler.path) def post_arg_parse_setup(config: configuration.NamespaceConfig) -> None: """Setup logging after command line arguments are parsed. This function assumes `pre_arg_parse_setup` was called earlier and the root logging configuration has not been modified. A rotating file logging handler is created and the buffered log messages are sent to that handler. Terminal logging output is set to the level requested by the user. :param certbot.configuration.NamespaceConfig config: Configuration object """ file_handler, file_path = setup_log_file_handler( config, 'letsencrypt.log', FILE_FMT) root_logger = logging.getLogger() memory_handler = stderr_handler = None for handler in root_logger.handlers: if isinstance(handler, ColoredStreamHandler): stderr_handler = handler elif isinstance(handler, MemoryHandler): memory_handler = handler msg = 'Previously configured logging handlers have been removed!' assert memory_handler is not None and stderr_handler is not None, msg root_logger.addHandler(file_handler) root_logger.removeHandler(memory_handler) temp_handler = getattr(memory_handler, 'target', None) memory_handler.setTarget(file_handler) # pylint: disable=no-member memory_handler.flush(force=True) # pylint: disable=unexpected-keyword-arg memory_handler.close() if temp_handler: temp_handler.close() if config.quiet: level = constants.QUIET_LOGGING_LEVEL elif config.verbose_level is not None: level = constants.DEFAULT_LOGGING_LEVEL - int(config.verbose_level) * 10 else: level = constants.DEFAULT_LOGGING_LEVEL - config.verbose_count * 10 stderr_handler.setLevel(level) logger.debug('Root logging level set at %d', level) if not config.quiet: print(f'Saving debug log to {file_path}', file=sys.stderr) sys.excepthook = functools.partial( post_arg_parse_except_hook, debug=config.debug, quiet=config.quiet, log_path=file_path) def setup_log_file_handler(config: configuration.NamespaceConfig, logfile: str, fmt: str) -> Tuple[logging.Handler, str]: """Setup file debug logging. :param certbot.configuration.NamespaceConfig config: Configuration object :param str logfile: basename for the log file :param str fmt: logging format string :returns: file handler and absolute path to the log file :rtype: tuple """ # TODO: logs might contain sensitive data such as contents of the # private key! #525 util.set_up_core_dir(config.logs_dir, 0o700, config.strict_permissions) log_file_path = os.path.join(config.logs_dir, logfile) try: handler = logging.handlers.RotatingFileHandler( log_file_path, maxBytes=2 ** 20, backupCount=config.max_log_backups) except IOError as error: raise errors.Error(util.PERM_ERR_FMT.format(error)) # 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.setFormatter(handler_formatter) return handler, log_file_path class ColoredStreamHandler(logging.StreamHandler): """Sends colored logging output to a stream. If the specified stream is not a tty, the class works like the standard `logging.StreamHandler`. Default red_level is `logging.WARNING`. :ivar bool colored: True if output should be colored :ivar bool red_level: The level at which to output """ def __init__(self, stream: Optional[IO] = None) -> None: super().__init__(stream) self.colored = (sys.stderr.isatty() if stream is None else stream.isatty()) self.red_level = logging.WARNING def format(self, record: logging.LogRecord) -> str: """Formats the string representation of record. :param logging.LogRecord record: Record to be formatted :returns: Formatted, string representation of record :rtype: str """ out = super().format(record) if self.colored and record.levelno >= self.red_level: return ''.join((util.ANSI_SGR_RED, out, util.ANSI_SGR_RESET)) return out class MemoryHandler(logging.handlers.MemoryHandler): """Buffers logging messages in memory until the buffer is flushed. This differs from `logging.handlers.MemoryHandler` in that flushing only happens when flush(force=True) is called. """ def __init__(self, target: Optional[logging.Handler] = None, capacity: int = 10000) -> None: # capacity doesn't matter because should_flush() is overridden super().__init__(capacity, target=target) def close(self) -> None: """Close the memory handler, but don't set the target to None.""" # This allows the logging module which may only have a weak # reference to the target handler to properly flush and close it. target = getattr(self, 'target') super().close() self.target = target def flush(self, force: bool = False) -> None: # pylint: disable=arguments-differ """Flush the buffer if force=True. If force=False, this call is a noop. :param bool force: True if the buffer should be flushed. """ # This method allows flush() calls in logging.shutdown to be a # noop so we can control when this handler is flushed. if force: super().flush() def shouldFlush(self, record: logging.LogRecord) -> bool: """Should the buffer be automatically flushed? :param logging.LogRecord record: log record to be considered :returns: False because the buffer should never be auto-flushed :rtype: bool """ return False class TempHandler(logging.StreamHandler): """Safely logs messages to a temporary file. The file is created with permissions 600. If no log records are sent to this handler, the temporary file is deleted when the handler is closed. :ivar str path: file system path to the temporary log file """ def __init__(self) -> None: self._workdir = tempfile.mkdtemp(prefix="certbot-log-") self.path = os.path.join(self._workdir, 'log') stream = util.safe_open(self.path, mode='w', chmod=0o600) super().__init__(stream) # Super constructor assigns the provided stream object to self.stream. # Let's help mypy be aware of this by giving a type hint. self.stream: IO[str] self._delete = True def emit(self, record: logging.LogRecord) -> None: """Log the specified logging record. :param logging.LogRecord record: Record to be formatted """ self._delete = False super().emit(record) def close(self) -> None: """Close the handler and the temporary log file. The temporary log file is deleted if it wasn't used. """ self.acquire() try: # StreamHandler.close() doesn't close the stream to allow a # stream like stderr to be used self.stream.close() if self._delete: shutil.rmtree(self._workdir) self._delete = False super().close() finally: self.release() def pre_arg_parse_except_hook(memory_handler: MemoryHandler, *args: Any, **kwargs: Any) -> None: """A simple wrapper around post_arg_parse_except_hook. The additional functionality provided by this wrapper is the memory handler will be flushed before Certbot exits. This allows us to write logging messages to a temporary file if we crashed before logging was fully configured. Since sys.excepthook isn't called on SystemExit exceptions, the memory handler will not be flushed in this case which prevents us from creating temporary log files when argparse exits because a command line argument was invalid or -h, --help, or --version was provided on the command line. :param MemoryHandler memory_handler: memory handler to flush :param tuple args: args for post_arg_parse_except_hook :param dict kwargs: kwargs for post_arg_parse_except_hook """ try: post_arg_parse_except_hook(*args, **kwargs) finally: # flush() is called here so messages logged during # post_arg_parse_except_hook are also flushed. memory_handler.flush(force=True) def post_arg_parse_except_hook(exc_type: Type[BaseException], exc_value: BaseException, trace: TracebackType, debug: bool, quiet: bool, log_path: str) -> None: """Logs fatal exceptions and reports them to the user. If debug is True, the full exception and traceback is shown to the user, otherwise, it is suppressed. sys.exit is always called with a nonzero status. :param type exc_type: type of the raised exception :param BaseException exc_value: raised exception :param traceback trace: traceback of where the exception was raised :param bool debug: True if the traceback should be shown to the user :param bool quiet: True if Certbot is running in quiet mode :param str log_path: path to file or directory containing the log """ exc_info = (exc_type, exc_value, trace) # Only print human advice if not running under --quiet def exit_func() -> None: if quiet: sys.exit(1) else: exit_with_advice(log_path) # constants.QUIET_LOGGING_LEVEL or higher should be used to # display message the user, otherwise, a lower level like # logger.DEBUG should be used if debug or not issubclass(exc_type, Exception): assert constants.QUIET_LOGGING_LEVEL <= logging.ERROR if exc_type is KeyboardInterrupt: logger.error('Exiting due to user request.') sys.exit(1) logger.error('Exiting abnormally:', exc_info=exc_info) else: logger.debug('Exiting abnormally:', exc_info=exc_info) # Use logger to print the error message to take advantage of # our logger printing warnings and errors in red text. if issubclass(exc_type, errors.Error): logger.error(str(exc_value)) exit_func() logger.error('An unexpected error occurred:') if messages.is_acme_error(exc_value): logger.error(display_util.describe_acme_error(cast(messages.Error, exc_value))) else: output = traceback.format_exception_only(exc_type, exc_value) # format_exception_only returns a list of strings each # terminated by a newline. We combine them into one string # and remove the final newline before passing it to # logger.error. logger.error(''.join(output).rstrip()) exit_func() def exit_with_advice(log_path: str) -> None: """Print a link to the community forums, the debug log path, and exit The message is printed to stderr and the program will exit with a nonzero status. :param str log_path: path to file or directory containing the log """ msg = ("Ask for help or search for solutions at https://community.letsencrypt.org. " "See the ") if os.path.isdir(log_path): msg += f'logfiles in {log_path} ' else: msg += f"logfile {log_path} " msg += 'or re-run Certbot with -v for more details.' sys.exit(msg) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/main.py0000664000175100017510000022232614561227515017112 0ustar00ericaerica"""Certbot main entry point.""" # pylint: disable=too-many-lines from contextlib import contextmanager import copy import functools import logging.handlers import sys from typing import cast from typing import Generator from typing import IO from typing import Iterable from typing import List from typing import Optional from typing import Tuple from typing import TypeVar from typing import Union import warnings import configobj import josepy as jose from josepy import b64 from acme import client as acme_client from acme import errors as acme_errors from acme import messages as acme_messages import certbot from certbot import configuration from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot import util from certbot._internal import account from certbot._internal import cert_manager from certbot._internal import cli from certbot._internal import client from certbot._internal import constants from certbot._internal import eff from certbot._internal import hooks from certbot._internal import log from certbot._internal import renewal from certbot._internal import snap_config from certbot._internal import storage from certbot._internal import updater from certbot._internal.display import obj as display_obj from certbot._internal.display import util as internal_display_util from certbot._internal.plugins import disco as plugins_disco from certbot._internal.plugins import selection as plug_sel from certbot.compat import filesystem from certbot.compat import misc from certbot.compat import os from certbot.display import ops as display_ops from certbot.display import util as display_util from certbot.plugins import enhancements USER_CANCELLED = ("User chose to cancel the operation and may " "reinvoke the client.") logger = logging.getLogger(__name__) def _suggest_donation_if_appropriate(config: configuration.NamespaceConfig) -> None: """Potentially suggest a donation to support Certbot. :param config: Configuration object :type config: configuration.NamespaceConfig :returns: `None` :rtype: None """ # don't prompt for donation if: # - renewing # - using the staging server (--staging or --dry-run) # - running with --quiet (display fd won't be available during atexit calls #8995) assert config.verb != "renew" if config.staging or config.quiet: return util.atexit_register( display_util.notification, "If you like Certbot, please consider supporting our work by:\n" " * Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n" " * Donating to EFF: https://eff.org/donate-le", pause=False ) def _get_and_save_cert(le_client: client.Client, config: configuration.NamespaceConfig, domains: Optional[List[str]] = None, certname: Optional[str] = None, lineage: Optional[storage.RenewableCert] = None ) -> Optional[storage.RenewableCert]: """Authenticate and enroll certificate. This method finds the relevant lineage, figures out what to do with it, then performs that action. Includes calls to hooks, various reports, checks, and requests for user input. :param config: Configuration object :type config: configuration.NamespaceConfig :param domains: List of domain names to get a certificate. Defaults to `None` :type domains: `list` of `str` :param certname: Name of new certificate. Defaults to `None` :type certname: str :param lineage: Certificate lineage object. Defaults to `None` :type lineage: storage.RenewableCert :returns: the issued certificate or `None` if doing a dry run :rtype: storage.RenewableCert or None :raises errors.Error: if certificate could not be obtained """ hooks.pre_hook(config) renewed_domains: List[str] = [] try: if lineage is not None: # Renewal, where we already know the specific lineage we're # interested in display_util.notify( "{action} for {domains}".format( action="Simulating renewal of an existing certificate" if config.dry_run else "Renewing an existing certificate", domains=internal_display_util.summarize_domain_list(domains or lineage.names()) ) ) renewal.renew_cert(config, domains, le_client, lineage) else: # TREAT AS NEW REQUEST if domains is None: raise errors.Error("Domain list cannot be none if the lineage is not set.") display_util.notify( "{action} for {domains}".format( action="Simulating a certificate request" if config.dry_run else "Requesting a certificate", domains=internal_display_util.summarize_domain_list(domains) ) ) lineage = le_client.obtain_and_enroll_certificate(domains, certname) if lineage is False: raise errors.Error("Certificate could not be obtained") if lineage is not None: hooks.deploy_hook(config, lineage.names(), lineage.live_dir) renewed_domains.extend(domains) finally: hooks.post_hook(config, renewed_domains) return lineage def _handle_unexpected_key_type_migration(config: configuration.NamespaceConfig, cert: storage.RenewableCert) -> bool: """ This function ensures that the user will not implicitly migrate an existing key from one type to another in the situation where a certificate for that lineage already exist and they have not provided explicitly --key-type and --cert-name. :param config: Current configuration provided by the client :param cert: Matching certificate that could be renewed :returns: Whether a key type migration is going ahead. :rtype: `bool` """ new_key_type = config.key_type.upper() cur_key_type = cert.private_key_type.upper() if new_key_type == cur_key_type: return False # If both --key-type and --cert-name are provided, we consider the user's intent to # be unambiguous: to change the key type of this lineage. is_confirmed_via_cli = config.set_by_user("key_type") and config.set_by_user("certname") # Failing that, we interactively prompt the user to confirm the change. if is_confirmed_via_cli or display_util.yesno( f'An {cur_key_type} certificate named {cert.lineagename} already exists. Do you want to ' f'update its key type to {new_key_type}?', yes_label='Update key type', no_label='Keep existing key type', default=False, force_interactive=False, ): return True # If --key-type was set on the CLI but the user did not confirm the key type change using # one of the two above methods, their intent is ambiguous. Error out. if config.set_by_user("key_type"): raise errors.Error( 'Are you trying to change the key type of the certificate named ' f'{cert.lineagename} from {cur_key_type} to {new_key_type}? Please provide ' 'both --cert-name and --key-type on the command line to confirm the change ' 'you are trying to make.' ) # The mismatch between the lineage's key type and config.key_type is caused by Certbot's # default value. The user is not asking for a key change: keep the key type of the existing # lineage. config.key_type = cur_key_type.lower() return False def _handle_subset_cert_request(config: configuration.NamespaceConfig, domains: Iterable[str], cert: storage.RenewableCert ) -> Tuple[str, Optional[storage.RenewableCert]]: """Figure out what to do if a previous cert had a subset of the names now requested :param config: Configuration object :type config: configuration.NamespaceConfig :param domains: List of domain names :type domains: `list` of `str` :param cert: Certificate object :type cert: storage.RenewableCert :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" :rtype: `tuple` of `str` """ _handle_unexpected_key_type_migration(config, cert) 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 display_util.yesno( question, "Expand", "Cancel", cli_flag="--expand", force_interactive=True): return "renew", cert display_util.notify( "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, cli.cli_command, " ".join(sys.argv[1:]), br=os.linesep )) raise errors.Error(USER_CANCELLED) def _handle_identical_cert_request(config: configuration.NamespaceConfig, lineage: storage.RenewableCert, ) -> Tuple[str, Optional[storage.RenewableCert]]: """Figure out what to do if a lineage has the same names as a previously obtained one :param config: Configuration object :type config: configuration.NamespaceConfig :param lineage: Certificate lineage object :type lineage: storage.RenewableCert :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" :rtype: `tuple` of `str` """ is_key_type_changing = _handle_unexpected_key_type_migration(config, lineage) if not lineage.ensure_deployed(): return "reinstall", lineage if is_key_type_changing or renewal.should_renew(config, lineage): return "renew", lineage if config.reinstall: # Set with --reinstall, force an identical certificate to be # reinstalled without further prompting. return "reinstall", lineage question = ( "You have an existing certificate that has exactly the same " "domains or certificate name you requested and isn't close to expiry." "{br}(ref: {0}){br}{br}What would you like to do?" ).format(lineage.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 certificate (may be subject to CA rate limits)"] response = display_util.menu(question, choices, default=0, force_interactive=True) if response[0] == display_util.CANCEL: # TODO: Add notification related to command-line options for # skipping the menu for this case. raise errors.Error( "Operation canceled. You may re-run the client.") if response[1] == 0: return "reinstall", lineage elif response[1] == 1: return "renew", lineage raise AssertionError('This is impossible') def _find_lineage_for_domains(config: configuration.NamespaceConfig, domains: List[str] ) -> Tuple[Optional[str], Optional[storage.RenewableCert]]: """Determine whether there are duplicated names and how to handle them (renew, reinstall, newcert, or raising an error to stop the client run if the user chooses to cancel the operation when prompted). :param config: Configuration object :type config: configuration.NamespaceConfig :param domains: List of domain names :type domains: `list` of `str` :returns: Two-element tuple containing desired new-certificate behavior as a string token ("reinstall", "renew", or "newcert"), plus either a RenewableCert instance or `None` if renewal shouldn't occur. :rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None` :raises errors.Error: If the user would like to rerun the client again. """ # Considering the possibility that the requested certificate is # related to an existing certificate. (config.duplicate, which # is set with --duplicate, skips all of this logic and forces any # kind of certificate to be obtained with renewal = False.) if config.duplicate: return "newcert", None # TODO: Also address superset case ident_names_cert, subset_names_cert = cert_manager.find_duplicative_certs(config, domains) # XXX ^ schoen is not sure whether that correctly reads the systemwide # configuration file. if ident_names_cert is None and subset_names_cert is None: return "newcert", None if ident_names_cert is not None: return _handle_identical_cert_request(config, ident_names_cert) elif subset_names_cert is not None: return _handle_subset_cert_request(config, domains, subset_names_cert) return None, None def _find_cert(config: configuration.NamespaceConfig, domains: List[str], certname: str ) -> Tuple[bool, Optional[storage.RenewableCert]]: """Finds an existing certificate object given domains and/or a certificate name. :param config: Configuration object :type config: configuration.NamespaceConfig :param domains: List of domain names :type domains: `list` of `str` :param certname: Name of certificate :type certname: str :returns: Two-element tuple of a boolean that indicates if this function should be followed by a call to fetch a certificate from the server, and either a RenewableCert instance or None. :rtype: `tuple` of `bool` and :class:`storage.RenewableCert` or `None` """ action, lineage = _find_lineage_for_domains_and_certname(config, domains, certname) if action == "reinstall": logger.info("Keeping the existing certificate") return (action != "reinstall"), lineage def _find_lineage_for_domains_and_certname( config: configuration.NamespaceConfig, domains: List[str], certname: str) -> Tuple[Optional[str], Optional[storage.RenewableCert]]: """Find appropriate lineage based on given domains and/or certname. :param config: Configuration object :type config: configuration.NamespaceConfig :param domains: List of domain names :type domains: `list` of `str` :param certname: Name of certificate :type certname: str :returns: Two-element tuple containing desired new-certificate behavior as a string token ("reinstall", "renew", or "newcert"), plus either a RenewableCert instance or None if renewal should not occur. :rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None` :raises errors.Error: If the user would like to rerun the client again. """ if not certname: return _find_lineage_for_domains(config, domains) lineage = cert_manager.lineage_for_certname(config, certname) if lineage: if domains: computed_domains = cert_manager.domains_for_certname(config, certname) if computed_domains and set(computed_domains) != set(domains): _handle_unexpected_key_type_migration(config, lineage) _ask_user_to_confirm_new_names(config, domains, certname, lineage.names()) # raises if no return "renew", lineage # unnecessarily specified domains or no domains specified return _handle_identical_cert_request(config, lineage) elif domains: return "newcert", None raise errors.ConfigurationError("No certificate with name {0} found. " "Use -d to specify domains, or run certbot certificates to see " "possible certificate names.".format(certname)) T = TypeVar("T") def _get_added_removed(after: Iterable[T], before: Iterable[T]) -> Tuple[List[T], List[T]]: """Get lists of items removed from `before` and a lists of items added to `after` """ added = list(set(after) - set(before)) removed = list(set(before) - set(after)) added.sort() removed.sort() return added, removed def _format_list(character: str, strings: Iterable[str]) -> str: """Format list with given character """ if not strings: formatted = "{br}(None)" else: formatted = "{br}{ch} " + "{br}{ch} ".join(strings) return formatted.format( ch=character, br=os.linesep ) def _ask_user_to_confirm_new_names(config: configuration.NamespaceConfig, new_domains: Iterable[str], certname: str, old_domains: Iterable[str]) -> None: """Ask user to confirm update cert certname to contain new_domains. :param config: Configuration object :type config: configuration.NamespaceConfig :param new_domains: List of new domain names :type new_domains: `list` of `str` :param certname: Name of certificate :type certname: str :param old_domains: List of old domain names :type old_domains: `list` of `str` :returns: None :rtype: None :raises errors.ConfigurationError: if cert name and domains mismatch """ if config.renew_with_new_domains: return added, removed = _get_added_removed(new_domains, old_domains) msg = ("You are updating certificate {0} to include new domain(s): {1}{br}{br}" "You are also removing previously included domain(s): {2}{br}{br}" "Did you intend to make this change?".format( certname, _format_list("+", added), _format_list("-", removed), br=os.linesep)) if not display_util.yesno(msg, "Update certificate", "Cancel", default=True): raise errors.ConfigurationError("Specified mismatched certificate name and domains.") def _find_domains_or_certname(config: configuration.NamespaceConfig, installer: Optional[interfaces.Installer], question: Optional[str] = None) -> Tuple[List[str], str]: """Retrieve domains and certname from config or user input. :param config: Configuration object :type config: configuration.NamespaceConfig :param installer: Installer object :type installer: interfaces.Installer :param `str` question: Overriding default 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 _report_next_steps(config: configuration.NamespaceConfig, installer_err: Optional[errors.Error], lineage: Optional[storage.RenewableCert], new_or_renewed_cert: bool = True) -> None: """Displays post-run/certonly advice to the user about renewal and installation. The output varies by runtime configuration and any errors encountered during installation. :param config: Configuration object :type config: configuration.NamespaceConfig :param installer_err: The installer/enhancement error encountered, if any. :type error: Optional[errors.Error] :param lineage: The resulting certificate lineage from the issuance, if any. :type lineage: Optional[storage.RenewableCert] :param bool new_or_renewed_cert: Whether the verb execution resulted in a certificate being saved (created or renewed). """ steps: List[str] = [] # If the installation or enhancement raised an error, show advice on trying again if installer_err: # Special case where either --nginx or --apache were used, causing us to # run the "installer" (i.e. reloading the nginx/apache config) if config.verb == 'certonly': steps.append( "The certificate was saved, but was not successfully loaded by the installer " f"({config.installer}) due to the installer failing to reload. " f"After fixing the error shown below, try reloading {config.installer} manually." ) else: steps.append( "The certificate was saved, but could not be installed (installer: " f"{config.installer}). After fixing the error shown below, try installing it again " f"by running:\n {cli.cli_command} install --cert-name " f"{_cert_name_from_config_or_lineage(config, lineage)}" ) # If a certificate was obtained or renewed, show applicable renewal advice if new_or_renewed_cert: if config.csr: steps.append( "Certificates created using --csr will not be renewed automatically by Certbot. " "You will need to renew the certificate before it expires, by running the same " "Certbot command again.") elif _is_interactive_only_auth(config): steps.append( "This certificate will not be renewed automatically. Autorenewal of " "--manual certificates requires the use of an authentication hook script " "(--manual-auth-hook) but one was not provided. To renew this certificate, repeat " f"this same {cli.cli_command} command before the certificate's expiry date." ) elif not config.preconfigured_renewal: steps.append( "The certificate will need to be renewed before it expires. Certbot can " "automatically renew the certificate in the background, but you may need " "to take steps to enable that functionality. " "See https://certbot.org/renewal-setup for instructions.") if not steps: return # TODO: refactor ANSI escapes during https://github.com/certbot/certbot/issues/8848 (bold_on, nl, bold_off) = [c if sys.stdout.isatty() and not config.quiet else '' \ for c in (util.ANSI_SGR_BOLD, '\n', util.ANSI_SGR_RESET)] print(bold_on, end=nl) display_util.notify("NEXT STEPS:") print(bold_off, end='') for step in steps: display_util.notify(f"- {step}") # If there was an installer error, segregate the error output with a trailing newline if installer_err: print() def _report_new_cert(config: configuration.NamespaceConfig, cert_path: Optional[str], fullchain_path: Optional[str], key_path: Optional[str] = None) -> None: """Reports the creation of a new certificate to the user. :param config: Configuration object :type config: configuration.NamespaceConfig :param cert_path: path to certificate :type cert_path: str :param fullchain_path: path to full chain :type fullchain_path: str :param key_path: path to private key, if available :type key_path: str :returns: `None` :rtype: None """ if config.dry_run: display_util.notify("The dry run was successful.") return assert cert_path and fullchain_path, "No certificates saved to report." renewal_msg = "" if config.preconfigured_renewal and not _is_interactive_only_auth(config): renewal_msg = ("\nCertbot has set up a scheduled task to automatically renew this " "certificate in the background.") display_util.notify( ("\nSuccessfully received certificate.\n" "Certificate is saved at: {cert_path}\n{key_msg}" "This certificate expires on {expiry}.\n" "These files will be updated when the certificate renews.{renewal_msg}{nl}").format( cert_path=fullchain_path, expiry=crypto_util.notAfter(cert_path).date(), key_msg="Key is saved at: {}\n".format(key_path) if key_path else "", renewal_msg=renewal_msg, nl="\n" if config.verb == "run" else "" # Normalize spacing across verbs ) ) def _is_interactive_only_auth(config: configuration.NamespaceConfig) -> bool: """ Whether the current authenticator params only support interactive renewal. """ # --manual without --manual-auth-hook can never autorenew if config.authenticator == "manual" and config.manual_auth_hook is None: return True return False def _csr_report_new_cert(config: configuration.NamespaceConfig, cert_path: Optional[str], chain_path: Optional[str], fullchain_path: Optional[str]) -> None: """ --csr variant of _report_new_cert. Until --csr is overhauled (#8332) this is transitional function to report the creation of a new certificate using --csr. TODO: remove this function and just call _report_new_cert when --csr is overhauled. :param config: Configuration object :type config: configuration.NamespaceConfig :param str cert_path: path to cert.pem :param str chain_path: path to chain.pem :param str fullchain_path: path to fullchain.pem """ if config.dry_run: display_util.notify("The dry run was successful.") return assert cert_path and fullchain_path, "No certificates saved to report." expiry = crypto_util.notAfter(cert_path).date() display_util.notify( ("\nSuccessfully received certificate.\n" "Certificate is saved at: {cert_path}\n" "Intermediate CA chain is saved at: {chain_path}\n" "Full certificate chain is saved at: {fullchain_path}\n" "This certificate expires on {expiry}.").format( cert_path=cert_path, chain_path=chain_path, fullchain_path=fullchain_path, expiry=expiry, ) ) def _determine_account(config: configuration.NamespaceConfig ) -> Tuple[account.Account, Optional[acme_client.ClientV2]]: """Determine which account to use. If ``config.account`` is ``None``, it will be updated based on the user input. Same for ``config.email``. :param config: Configuration object :type config: configuration.NamespaceConfig :returns: Account and optionally ACME client API (biproduct of new registration). :rtype: tuple of :class:`certbot._internal.account.Account` and :class:`acme.client.Client` :raises errors.Error: If unable to register an account with ACME server """ def _tos_cb(terms_of_service: str) -> None: if config.tos: return msg = ("Please read the Terms of Service at {0}. You " "must agree in order to register with the ACME " "server. Do you agree?".format(terms_of_service)) result = display_util.yesno(msg, cli_flag="--agree-tos", force_interactive=True) if not result: raise errors.Error( "Registration cannot proceed without accepting " "Terms of Service.") account_storage = account.AccountFileStorage(config) acme: Optional[acme_client.ClientV2] = None if config.account is not None: acc = account_storage.load(config.account) else: accounts = account_storage.find_all() if len(accounts) > 1: potential_acc = display_ops.choose_account(accounts) if not potential_acc: raise errors.Error("No account has been chosen.") acc = potential_acc elif len(accounts) == 1: acc = accounts[0] else: # no account registered yet if config.email is None and not config.register_unsafely_without_email: config.email = display_ops.get_email() try: acc, acme = client.register( config, account_storage, tos_cb=_tos_cb) display_util.notify("Account registered.") except errors.MissingCommandlineFlag: raise except (errors.Error, acme_messages.Error) as err: logger.debug("", exc_info=True) if acme_messages.is_acme_error(err): err_msg = internal_display_util.describe_acme_error( cast(acme_messages.Error, err)) err_msg = f"Error returned by the ACME server: {err_msg}" else: err_msg = str(err) raise errors.Error( f"Unable to register an account with ACME server. {err_msg}") config.account = acc.id return acc, acme def _delete_if_appropriate(config: configuration.NamespaceConfig) -> None: """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: configuration.NamespaceConfig :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 ... """ attempt_deletion = config.delete_after_revoke if attempt_deletion is None: msg = ("Would you like to delete the certificate(s) you just revoked, " "along with all earlier and later versions of the certificate?") attempt_deletion = display_util.yesno(msg, yes_label="Yes (recommended)", no_label="No", force_interactive=True, default=True) if not attempt_deletion: 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), encoding='utf-8', default_encoding='utf-8'), config, config.certname) try: cert_manager.match_and_check_overlaps(config, [lambda x: archive_dir], lambda x: x.archive_dir, lambda x: x.lineagename) except errors.OverlappingMatchFound: logger.warning("Not deleting revoked certificates due to overlapping archive dirs. " "More than one certificate is using %s", archive_dir) 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 _init_le_client(config: configuration.NamespaceConfig, authenticator: Optional[interfaces.Authenticator], installer: Optional[interfaces.Installer]) -> client.Client: """Initialize Let's Encrypt Client :param config: Configuration object :type config: configuration.NamespaceConfig :param authenticator: Acme authentication handler :type authenticator: Optional[interfaces.Authenticator] :param installer: Installer object :type installer: interfaces.Installer :returns: client: Client object :rtype: client.Client """ acc: Optional[account.Account] if authenticator is not None: # if authenticator was given, then we will need account... acc, acme = _determine_account(config) logger.debug("Picked account: %r", acc) else: acc, acme = None, None return client.Client(config, acc, authenticator, installer, acme=acme) def unregister(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Deactivate account on server :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` or a string indicating an error :rtype: None or str """ account_storage = account.AccountFileStorage(config) accounts = account_storage.find_all() if not accounts: return f"Could not find existing account for server {config.server}." prompt = ("Are you sure you would like to irrevocably deactivate " "your account?") wants_deactivate = display_util.yesno(prompt, yes_label='Deactivate', no_label='Abort', default=True) if not wants_deactivate: return "Deactivation aborted." acc, acme = _determine_account(config) cb_client = client.Client(config, acc, None, None, acme=acme) if not cb_client.acme: raise errors.Error("ACME client is not set.") # delete on boulder cb_client.acme.deactivate_registration(acc.regr) account_files = account.AccountFileStorage(config) # delete local account files account_files.delete(config.account) display_util.notify("Account deactivated.") return None def register(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Create accounts on the server. :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` or a string indicating an error :rtype: None or str """ # Portion of _determine_account logic to see whether accounts already # exist or not. account_storage = account.AccountFileStorage(config) accounts = account_storage.find_all() if accounts: # TODO: add a flag to register a duplicate account (this will # also require extending _determine_account's behavior # or else extracting the registration code from there) return ("There is an existing account; registration of a " "duplicate account with this command is currently " "unsupported.") # _determine_account will register an account _determine_account(config) return None def update_account(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Modify accounts on the server. :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` or a string indicating an error :rtype: None or str """ # Portion of _determine_account logic to see whether accounts already # exist or not. account_storage = account.AccountFileStorage(config) accounts = account_storage.find_all() if not accounts: return f"Could not find an existing account for server {config.server}." if config.email is None and not config.register_unsafely_without_email: config.email = display_ops.get_email(optional=False) acc, acme = _determine_account(config) cb_client = client.Client(config, acc, None, None, acme=acme) if not cb_client.acme: raise errors.Error("ACME client is not set.") # Empty list of contacts in case the user is removing all emails acc_contacts: Iterable[str] = () if config.email: acc_contacts = ['mailto:' + email for email in config.email.split(',')] # We rely on an exception to interrupt this process if it didn't work. prev_regr_uri = acc.regr.uri acc.regr = cb_client.acme.update_registration(acc.regr.update( body=acc.regr.body.update(contact=acc_contacts))) # A v1 account being used as a v2 account will result in changing the uri to # the v2 uri. Since it's the same object on disk, put it back to the v1 uri # so that we can also continue to use the account object with acmev1. acc.regr = acc.regr.update(uri=prev_regr_uri) account_storage.update_regr(acc) if not config.email: display_util.notify("Any contact information associated " "with this account has been removed.") else: eff.prepare_subscription(config, acc) display_util.notify("Your e-mail address was updated to {0}.".format(config.email)) return None def show_account(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Fetch account info from the ACME server and show it to the user. :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` or a string indicating an error :rtype: None or str """ # Portion of _determine_account logic to see whether accounts already # exist or not. account_storage = account.AccountFileStorage(config) accounts = account_storage.find_all() if not accounts: return f"Could not find an existing account for server {config.server}." acc, acme = _determine_account(config) cb_client = client.Client(config, acc, None, None, acme=acme) if not cb_client.acme: raise errors.Error("ACME client is not set.") regr = cb_client.acme.query_registration(acc.regr) output = [f"Account details for server {config.server}:", f" Account URL: {regr.uri}"] thumbprint = b64.b64encode(acc.key.thumbprint()).decode() output.append(f" Account Thumbprint: {thumbprint}") emails = [] for contact in regr.body.contact: if contact.startswith('mailto:'): emails.append(contact[7:]) output.append(" Email contact{}: {}".format( "s" if len(emails) > 1 else "", ", ".join(emails) if len(emails) > 0 else "none")) display_util.notify("\n".join(output)) return None def _cert_name_from_config_or_lineage(config: configuration.NamespaceConfig, lineage: Optional[storage.RenewableCert]) -> Optional[str]: if lineage: return lineage.lineagename elif config.certname: return config.certname try: cert_name = cert_manager.cert_path_to_lineage(config) return cert_name except errors.Error: pass return None def _install_cert(config: configuration.NamespaceConfig, le_client: client.Client, domains: List[str], lineage: Optional[storage.RenewableCert] = None) -> None: """Install a cert :param config: Configuration object :type config: configuration.NamespaceConfig :param le_client: Client object :type le_client: client.Client :param domains: List of domains :type domains: `list` of `str` :param lineage: Certificate lineage object. Defaults to `None` :type lineage: storage.RenewableCert :returns: `None` :rtype: None """ path_provider: Union[storage.RenewableCert, configuration.NamespaceConfig] = lineage if lineage else config assert path_provider.cert_path is not None le_client.deploy_certificate(domains, path_provider.key_path, path_provider.cert_path, path_provider.chain_path, path_provider.fullchain_path) le_client.enhance_config(domains, path_provider.chain_path) def install(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Install a previously obtained cert in a server. :param config: Configuration object :type config: configuration.NamespaceConfig :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry :returns: `None` or the error message :rtype: None or str """ # XXX: Update for renewer/RenewableCert # FIXME: be consistent about whether errors are raised or returned from # this function ... try: installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "install") except errors.PluginSelectionError as e: return str(e) custom_cert = (config.key_path and config.cert_path) if not config.certname and not custom_cert: certname_question = "Which certificate would you like to install?" config.certname = cert_manager.get_certnames( config, "install", allow_multiple=False, custom_prompt=certname_question)[0] if not enhancements.are_supported(config, installer): raise errors.NotSupportedError("One ore more of the requested enhancements " "are not supported by the selected installer") # If cert-path is defined, populate missing (ie. not overridden) values. # Unfortunately this can't be done in argument parser, as certificate # manager needs the access to renewal directory paths if config.certname: config = _populate_from_certname(config) elif enhancements.are_requested(config): # Preflight config check raise errors.ConfigurationError("One or more of the requested enhancements " "require --cert-name to be provided") if config.key_path and config.cert_path: _check_certificate_and_key(config) domains, _ = _find_domains_or_certname(config, installer) le_client = _init_le_client(config, authenticator=None, installer=installer) _install_cert(config, le_client, domains) else: raise errors.ConfigurationError("Path to certificate or key was not defined. " "If your certificate is managed by Certbot, please use --cert-name " "to define which certificate you would like to install.") if enhancements.are_requested(config): # In the case where we don't have certname, we have errored out already lineage = cert_manager.lineage_for_certname(config, config.certname) enhancements.enable(lineage, domains, installer, config) return None def _populate_from_certname(config: configuration.NamespaceConfig) -> configuration.NamespaceConfig: """Helper function for install to populate missing config values from lineage defined by --cert-name.""" lineage = cert_manager.lineage_for_certname(config, config.certname) if not lineage: return config if not config.key_path: config.key_path = lineage.key_path if not config.cert_path: config.cert_path = lineage.cert_path if not config.chain_path: config.chain_path = lineage.chain_path if not config.fullchain_path: config.fullchain_path = lineage.fullchain_path return config def _check_certificate_and_key(config: configuration.NamespaceConfig) -> None: if not os.path.isfile(filesystem.realpath(config.cert_path)): raise errors.ConfigurationError("Error while reading certificate from path " "{0}".format(config.cert_path)) if not os.path.isfile(filesystem.realpath(config.key_path)): raise errors.ConfigurationError("Error while reading private key from path " "{0}".format(config.key_path)) def plugins_cmd(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> None: """List server software plugins. :param config: Configuration object :type config: configuration.NamespaceConfig :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry :returns: `None` :rtype: None """ logger.debug("Expected interfaces: %s", config.ifaces) ifaces = [] if config.ifaces is None else config.ifaces filtered = plugins.visible().ifaces(ifaces) logger.debug("Filtered plugins: %r", filtered) notify = functools.partial(display_util.notification, pause=False) if not config.init and not config.prepare: notify(str(filtered)) return filtered.init(config) logger.debug("Filtered plugins: %r", filtered) if not config.prepare: notify(str(filtered)) return filtered.prepare() available = filtered.available() logger.debug("Prepared plugins: %s", available) notify(str(available)) def enhance(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Add security enhancements to existing configuration :param config: Configuration object :type config: configuration.NamespaceConfig :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry :returns: `None` or a string indicating an error :rtype: None or str """ 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.error(msg, cli.cli_command) 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 cert_domains is None: raise errors.Error("Could not find the list of domains for the given certificate name.") 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 lineage: raise errors.Error("Could not find the lineage for the given certificate name.") 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, redirect_default=False) if enhancements.are_requested(config): enhancements.enable(lineage, domains, installer, config) return None def rollback(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> None: """Rollback server configuration changes made during install. :param config: Configuration object :type config: configuration.NamespaceConfig :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry :returns: `None` :rtype: None """ client.rollback(config.installer, config.checkpoints, config, plugins) def update_symlinks(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> None: """Update the certificate file family symlinks Use the information in the config file to make symlinks point to the correct archive directory. :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` :rtype: None """ warnings.warn("update_symlinks is deprecated and will be removed", PendingDeprecationWarning) cert_manager.update_live_symlinks(config) def rename(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> None: """Rename a certificate Use the information in the config file to rename an existing lineage. :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` :rtype: None """ cert_manager.rename_lineage(config) def delete(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> None: """Delete a certificate Use the information in the config file to delete an existing lineage. :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` :rtype: None """ cert_manager.delete(config) def certificates(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> None: """Display information about certs configured with Certbot :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` :rtype: None """ cert_manager.certificates(config) def revoke(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Revoke a previously obtained certificate. :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` or string indicating error in case of error :rtype: None or str """ # For user-agent construction config.installer = config.authenticator = None if config.cert_path is None and config.certname: # When revoking via --cert-name, take the cert path and server from renewalparams lineage = storage.RenewableCert( storage.renewal_file_for_certname(config, config.certname), config) config.cert_path = lineage.cert_path # --server takes priority over lineage.server if lineage.server and not config.set_by_user("server"): config.server = lineage.server elif not config.cert_path or (config.cert_path and config.certname): # intentionally not supporting --cert-path & --cert-name together, # to avoid dealing with mismatched values raise errors.Error("Error! Exactly one of --cert-path or --cert-name must be specified!") if config.key_path is not None: # revocation by cert key logger.debug("Revoking %s using certificate key %s", config.cert_path, config.key_path) crypto_util.verify_cert_matches_priv_key(config.cert_path, config.key_path) with open(config.key_path, 'rb') as f: key = jose.JWK.load(f.read()) acme = client.acme_from_config_key(config, key) else: # revocation by account key logger.debug("Revoking %s using Account Key", config.cert_path) acc, _ = _determine_account(config) acme = client.acme_from_config_key(config, acc.key, acc.regr) with open(config.cert_path, 'rb') as f: cert = crypto_util.pyopenssl_load_certificate(f.read())[0] logger.debug("Reason code for revocation: %s", config.reason) try: acme.revoke(jose.ComparableX509(cert), config.reason) _delete_if_appropriate(config) except acme_errors.ClientError as e: return str(e) display_ops.success_revocation(config.cert_path) return None def run(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Obtain a certificate and install. :param config: Configuration object :type config: configuration.NamespaceConfig :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry :returns: `None` :rtype: None """ # TODO: Make run as close to auth + install as possible # Possible difficulties: config.csr was hacked into auth try: installer, authenticator = plug_sel.choose_configurator_plugins(config, plugins, "run") except errors.PluginSelectionError as e: return str(e) if config.must_staple and installer and "staple-ocsp" not in installer.supported_enhancements(): raise errors.NotSupportedError( "Must-Staple extension requested, but OCSP stapling is not supported by the selected " f"installer ({config.installer})\n\n" "You can either:\n" " * remove the --must-staple option from the command line and obtain a certificate " "without the Must-Staple extension, or;\n" " * use the `certonly` subcommand and manually install the certificate into the " "intended service (e.g. webserver). You must also then manually enable OCSP stapling, " "as it is required for certificates with the Must-Staple extension to " "function properly.\n" " * choose a different installer plugin (such as --nginx or --apache), if possible." ) # Preflight check for enhancement support by the selected installer if not enhancements.are_supported(config, installer): raise errors.NotSupportedError("One ore more of the requested enhancements " "are not supported by the selected installer") # TODO: Handle errors from _init_le_client? le_client = _init_le_client(config, authenticator, installer) domains, certname = _find_domains_or_certname(config, installer) should_get_cert, lineage = _find_cert(config, domains, certname) new_lineage = lineage if should_get_cert: new_lineage = _get_and_save_cert(le_client, config, domains, certname, lineage) cert_path = new_lineage.cert_path if new_lineage else None fullchain_path = new_lineage.fullchain_path if new_lineage else None key_path = new_lineage.key_path if new_lineage else None if should_get_cert: _report_new_cert(config, cert_path, fullchain_path, key_path) # The installer error, if any, is being stored as a value here, in order to first print # relevant advice in a nice way, before re-raising the error for normal processing. installer_err: Optional[errors.Error] = None try: _install_cert(config, le_client, domains, new_lineage) if enhancements.are_requested(config) and new_lineage: enhancements.enable(new_lineage, domains, installer, config) if lineage is None or not should_get_cert: display_ops.success_installation(domains) else: display_ops.success_renewal(domains) except errors.Error as e: installer_err = e finally: _report_next_steps(config, installer_err, new_lineage, new_or_renewed_cert=should_get_cert) # If the installer did fail, re-raise the error to bail out if installer_err: raise installer_err _suggest_donation_if_appropriate(config) eff.handle_subscription(config, le_client.account) return None def _csr_get_and_save_cert(config: configuration.NamespaceConfig, le_client: client.Client) -> Tuple[ Optional[str], Optional[str], Optional[str]]: """Obtain a cert using a user-supplied CSR This works differently in the CSR case (for now) because we don't have the privkey, and therefore can't construct the files for a lineage. So we just save the cert & chain to disk :/ :param config: Configuration object :type config: configuration.NamespaceConfig :param client: Client object :type client: client.Client :returns: `cert_path`, `chain_path` and `fullchain_path` as absolute paths to the actual files, or None for each if it's a dry-run. :rtype: `tuple` of `str` """ csr, _ = config.actual_csr csr_names = crypto_util.get_names_from_req(csr.data) display_util.notify( "{action} for {domains}".format( action="Simulating a certificate request" if config.dry_run else "Requesting a certificate", domains=internal_display_util.summarize_domain_list(csr_names) ) ) cert, chain = le_client.obtain_certificate_from_csr(csr) if config.dry_run: logger.debug( "Dry run: skipping saving certificate to %s", config.cert_path) return None, None, None cert_path, chain_path, fullchain_path = le_client.save_certificate( cert, chain, os.path.normpath(config.cert_path), os.path.normpath(config.chain_path), os.path.normpath(config.fullchain_path)) return cert_path, chain_path, fullchain_path def renew_cert(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry, lineage: storage.RenewableCert) -> None: """Renew & save an existing cert. Do not install it. :param config: Configuration object :type config: configuration.NamespaceConfig :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry :param lineage: Certificate lineage object :type lineage: storage.RenewableCert :returns: `None` :rtype: None :raises errors.PluginSelectionError: MissingCommandlineFlag if supplied parameters do not pass """ # installers are used in auth mode to determine domain names installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly") le_client = _init_le_client(config, auth, installer) renewed_lineage = _get_and_save_cert(le_client, config, lineage=lineage) if not renewed_lineage: raise errors.Error("An existing certificate for the given name could not be found.") if installer and not config.dry_run: # In case of a renewal, reload server to pick up new certificate. updater.run_renewal_deployer(config, renewed_lineage, installer) display_util.notify(f"Reloading {config.installer} server after certificate renewal") installer.restart() def certonly(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> None: """Authenticate & obtain cert, but do not install it. This implements the 'certonly' subcommand. :param config: Configuration object :type config: configuration.NamespaceConfig :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry :returns: `None` :rtype: None :raises errors.Error: If specified plugin could not be used """ # SETUP: Select plugins and construct a client instance # installers are used in auth mode to determine domain names installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly") le_client = _init_le_client(config, auth, installer) if config.csr: cert_path, chain_path, fullchain_path = _csr_get_and_save_cert(config, le_client) _csr_report_new_cert(config, cert_path, chain_path, fullchain_path) _report_next_steps(config, None, None, new_or_renewed_cert=not config.dry_run) _suggest_donation_if_appropriate(config) eff.handle_subscription(config, le_client.account) return domains, certname = _find_domains_or_certname(config, installer) should_get_cert, lineage = _find_cert(config, domains, certname) if not should_get_cert: display_util.notification("Certificate not yet due for renewal; no action taken.", pause=False) return lineage = _get_and_save_cert(le_client, config, domains, certname, lineage) # If a new cert was issued and we were passed an installer, we can safely # run `installer.restart()` to load the newly issued certificate installer_err: Optional[errors.Error] = None if lineage and installer and not config.dry_run: logger.info("Reloading %s server after certificate issuance", config.installer) try: installer.restart() except errors.Error as e: installer_err = e cert_path = lineage.cert_path if lineage else None fullchain_path = lineage.fullchain_path if lineage else None key_path = lineage.key_path if lineage else None _report_new_cert(config, cert_path, fullchain_path, key_path) _report_next_steps(config, installer_err, lineage, new_or_renewed_cert=should_get_cert and not config.dry_run) if installer_err: raise installer_err _suggest_donation_if_appropriate(config) eff.handle_subscription(config, le_client.account) def renew(config: configuration.NamespaceConfig, unused_plugins: plugins_disco.PluginsRegistry) -> None: """Renew previously-obtained certificates. :param config: Configuration object :type config: configuration.NamespaceConfig :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry :returns: `None` :rtype: None """ renewed_domains: List[str] = [] failed_domains: List[str] = [] try: renewed_domains, failed_domains = renewal.handle_renewal_request(config) finally: hooks.run_saved_post_hooks(renewed_domains, failed_domains) def make_or_verify_needed_dirs(config: configuration.NamespaceConfig) -> None: """Create or verify existence of config, work, and hook directories. :param config: Configuration object :type config: configuration.NamespaceConfig :returns: `None` :rtype: None """ util.set_up_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, config.strict_permissions) # Ensure the working directory has the expected mode, even under stricter umask settings with filesystem.temp_umask(0o022): util.set_up_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, config.strict_permissions) hook_dirs = (config.renewal_pre_hooks_dir, config.renewal_deploy_hooks_dir, config.renewal_post_hooks_dir,) for hook_dir in hook_dirs: util.make_or_verify_dir(hook_dir, strict=config.strict_permissions) def _report_reconfigure_results(renewal_file: str, orig_renewal_conf: configobj.ConfigObj) -> None: """Reports the outcome of certificate renewal reconfiguration to the user. :param renewal_file: Path to the cert's renewal file :type renewal_file: str :param orig_renewal_conf: Loaded original renewal configuration :type orig_renewal_conf: configobj.ConfigObj :returns: `None` :rtype: None """ try: final_renewal_conf = configobj.ConfigObj( renewal_file, encoding='utf-8', default_encoding='utf-8') except configobj.ConfigObjError: raise errors.CertStorageError( f'error parsing {renewal_file}') orig_renewal_params = orig_renewal_conf['renewalparams'] final_renewal_params = final_renewal_conf['renewalparams'] if final_renewal_params == orig_renewal_params: success_message = '\nNo changes were made to the renewal configuration.' else: success_message = '\nSuccessfully updated configuration.' + \ '\nChanges will apply when the certificate renews.' display_util.notify(success_message) def reconfigure(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> None: """Allow the user to set new configuration options for an existing certificate without forcing renewal. This can be used for things like authenticator, installer, and hooks, but not for the domains on the cert, since those are only saved in the cert. :param config: Configuration object :type config: configuration.NamespaceConfig :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry :raises errors.Error: if the dry run fails :raises errors.ConfigurationError: if certificate could not be loaded """ if config.domains: raise errors.ConfigurationError("You have specified domains, but this function cannot " "be used to modify the domains in a certificate. If you would like to do so, follow " "the instructions at https://certbot.org/change-cert-domain. Otherwise, remove the " "domains from the command to continue reconfiguring. You can specify which certificate " "you want on the command line with flag --cert-name instead.") # While we could technically allow domains to be used to specify the certificate in addition to # --cert-name, there's enough complexity with matching certs to domains that it's not worth it, # to say nothing of the difficulty in explaining what exactly this subcommand can modify # To make sure that the requested changes work, we're going to do a dry run, and only save # upon success. First, modify the config as the user requested. if not config.certname: certname_question = "Which certificate would you like to reconfigure?" config.certname = cert_manager.get_certnames( config, "reconfigure", allow_multiple=False, custom_prompt=certname_question)[0] certname = config.certname try: renewal_file = storage.renewal_file_for_certname(config, certname) except errors.CertStorageError: raise errors.ConfigurationError(f"An existing certificate with name {certname} could not " "be found. Run `certbot certificates` to list available certificates.") # figure this out before we modify config if config.deploy_hook and not config.run_deploy_hooks: msg = ("You are attempting to set a --deploy-hook. Would you like Certbot to run deploy " "hooks when it performs a dry run with the new settings? This will run all " "relevant deploy hooks, including directory hooks, unless --no-directory-hooks " "is set. This will use the current active certificate, and not the temporary test " "certificate acquired during the dry run.") config.run_deploy_hooks = display_util.yesno(msg,"Run deploy hooks", "Do not run deploy hooks", default=False) # cache previous version for later comparison try: orig_renewal_conf = configobj.ConfigObj( renewal_file, encoding='utf-8', default_encoding='utf-8') except configobj.ConfigObjError: raise errors.CertStorageError( f"error parsing {renewal_file}") lineage_config = copy.deepcopy(config) try: renewal_candidate = renewal.reconstitute(lineage_config, renewal_file) except Exception as e: # pylint: disable=broad-except raise errors.ConfigurationError(f"Renewal configuration file {renewal_file} " f"(cert: {certname}) produced an unexpected error: {e}.") if not renewal_candidate: raise errors.ConfigurationError("Could not load certificate. See logs for errors.") renewalparams = orig_renewal_conf['renewalparams'] # If server was set but hasn't changed and no account is loaded, # load the old account because reconstitute won't have if lineage_config.set_by_user('server') and lineage_config.server == renewalparams['server']\ and lineage_config.account is None: lineage_config.account = renewalparams['account'] for param in ('account', 'server',): if getattr(lineage_config, param) != renewalparams.get(param): msg = ("Using reconfigure to change the ACME account or server is not supported. " "If you would like to do so, use renew with the --force-renewal flag instead " "of reconfigure. Note that doing so will count against any rate limits. For " "more information on this method, see " "https://certbot.org/renew-reconfiguration") raise errors.ConfigurationError(msg) # this is where lineage_config gets fully filled out (e.g. --apache will set auth and installer) installer, auth = plug_sel.choose_configurator_plugins(lineage_config, plugins, "certonly") # make a deep copy of lineage_config because we're about to modify it for a test dry run dry_run_lineage_config = copy.deepcopy(lineage_config) # we also set noninteractive_mode to more accurately simulate renewal (since `certbot renew` # implies noninteractive mode) and to avoid prompting the user as changes made to # dry_run_lineage_config beyond this point will not be applied to the original lineage_config dry_run_lineage_config.noninteractive_mode = True dry_run_lineage_config.dry_run = True cli.set_test_server_options("reconfigure", dry_run_lineage_config) le_client = _init_le_client(dry_run_lineage_config, auth, installer) # renews cert as dry run to test that the new values are ok # at this point, renewal_candidate.configuration has the old values, but will use # the values from lineage_config when doing the dry run _get_and_save_cert(le_client, dry_run_lineage_config, certname=certname, lineage=renewal_candidate) # this function will update lineage.configuration with the new values, and save it to disk # use the pre-dry-run version renewal_candidate.save_new_config_values(lineage_config) _report_reconfigure_results(renewal_file, orig_renewal_conf) @contextmanager def make_displayer(config: configuration.NamespaceConfig ) -> Generator[Union[display_obj.NoninteractiveDisplay, display_obj.FileDisplay], None, None]: """Creates a display object appropriate to the flags in the supplied config. :param config: Configuration object :returns: Display object """ displayer: Union[None, display_obj.NoninteractiveDisplay, display_obj.FileDisplay] = None devnull: Optional[IO] = None if config.quiet: config.noninteractive_mode = True devnull = open(os.devnull, "w") # pylint: disable=consider-using-with displayer = display_obj.NoninteractiveDisplay(devnull) elif config.noninteractive_mode: displayer = display_obj.NoninteractiveDisplay(sys.stdout) else: displayer = display_obj.FileDisplay( sys.stdout, config.force_interactive) try: yield displayer finally: if devnull: devnull.close() def main(cli_args: Optional[List[str]] = None) -> Optional[Union[str, int]]: """Run Certbot. :param cli_args: command line to Certbot, defaults to ``sys.argv[1:]`` :type cli_args: `list` of `str` :returns: value for `sys.exit` about the exit status of Certbot :rtype: `str` or `int` or `None` """ if not cli_args: cli_args = sys.argv[1:] log.pre_arg_parse_setup() if os.environ.get('CERTBOT_SNAPPED') == 'True': cli_args = snap_config.prepare_env(cli_args) plugins = plugins_disco.PluginsRegistry.find_all() logger.debug("certbot version: %s", certbot.__version__) logger.debug("Location of certbot entry point: %s", sys.argv[0]) # do not log `config`, as it contains sensitive data (e.g. revoke --key)! logger.debug("Arguments: %r", cli_args) logger.debug("Discovered plugins: %r", plugins) # Some releases of Windows require escape sequences to be enable explicitly misc.prepare_virtual_console() # note: arg parser internally handles --help (and exits afterwards) config = cli.prepare_and_parse_args(plugins, cli_args) # On windows, shell without administrative right cannot create symlinks required by certbot. # So we check the rights before continuing. misc.raise_for_non_administrative_windows_rights() try: log.post_arg_parse_setup(config) make_or_verify_needed_dirs(config) except errors.Error: # Let plugins_cmd be run as un-privileged user. if config.func != plugins_cmd: # pylint: disable=comparison-with-callable raise with make_displayer(config) as displayer: display_obj.set_display(displayer) return config.func(config, plugins) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3310835 certbot-2.9.0/certbot/_internal/plugins/0000775000175100017510000000000014561227516017267 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/plugins/__init__.py0000664000175100017510000000002714561227515021376 0ustar00ericaerica"""Certbot plugins.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/plugins/disco.py0000664000175100017510000002601014561227515020740 0ustar00ericaerica"""Utilities for plugins discovery and selection.""" import logging import sys from typing import Callable from typing import cast from typing import Dict from typing import Iterable from typing import Iterator from typing import List from typing import Mapping from typing import Optional from typing import Type from typing import Union from certbot import configuration from certbot import errors from certbot import interfaces from certbot._internal import constants from certbot.compat import os from certbot.errors import Error if sys.version_info >= (3, 10): # pragma: no cover import importlib.metadata as importlib_metadata else: import importlib_metadata logger = logging.getLogger(__name__) PLUGIN_INTERFACES = [interfaces.Authenticator, interfaces.Installer, interfaces.Plugin] """Interfaces that should be listed in `certbot plugins` output""" class PluginEntryPoint: """Plugin entry point.""" # this object is mutable, don't allow it to be hashed! __hash__ = None # type: ignore def __init__(self, entry_point: importlib_metadata.EntryPoint) -> None: self.name = self.entry_point_to_plugin_name(entry_point) self.plugin_cls: Type[interfaces.Plugin] = entry_point.load() self.entry_point = entry_point self.warning_message: Optional[str] = None self._initialized: Optional[interfaces.Plugin] = None self._prepared: Optional[Union[bool, Error]] = None def check_name(self, name: Optional[str]) -> bool: """Check if the name refers to this plugin.""" if name == self.name: return True return False @classmethod def entry_point_to_plugin_name(cls, entry_point: importlib_metadata.EntryPoint) -> str: """Unique plugin name for an ``entry_point``""" return entry_point.name @property def description(self) -> str: """Description of the plugin.""" return self.plugin_cls.description @property def description_with_name(self) -> str: """Description with name. Handy for UI.""" return "{0} ({1})".format(self.description, self.name) @property def long_description(self) -> str: """Long description of the plugin.""" return getattr(self.plugin_cls, "long_description", self.description) @property def hidden(self) -> bool: """Should this plugin be hidden from UI?""" return getattr(self.plugin_cls, "hidden", False) def ifaces(self, *ifaces_groups: Iterable[Type]) -> bool: """Does plugin implement specified interface groups?""" return not ifaces_groups or any( all(issubclass(self.plugin_cls, iface) for iface in ifaces) for ifaces in ifaces_groups) @property def initialized(self) -> bool: """Has the plugin been initialized already?""" return self._initialized is not None def init(self, config: Optional[configuration.NamespaceConfig] = None) -> interfaces.Plugin: """Memoized plugin initialization.""" if not self._initialized: # For plugins implementing ABCs Plugin, Authenticator or Installer, the following # line will raise an exception if some implementations of abstract methods are missing. self._initialized = self.plugin_cls(config, self.name) return self._initialized @property def prepared(self) -> bool: """Has the plugin been prepared already?""" if not self.initialized: logger.debug(".prepared called on uninitialized %r", self) return self._prepared is not None def prepare(self) -> Union[bool, Error]: """Memoized plugin preparation.""" if self._initialized is None: raise ValueError("Plugin is not initialized.") if self._prepared is None: try: self._initialized.prepare() except errors.MisconfigurationError as error: logger.debug("Misconfigured %r: %s", self, error, exc_info=True) self._prepared = error except errors.NoInstallationError as error: logger.debug( "No installation (%r): %s", self, error, exc_info=True) self._prepared = error except errors.PluginError as error: logger.debug("Other error:(%r): %s", self, error, exc_info=True) self._prepared = error else: self._prepared = True # Mypy seems to fail to understand the actual type here, let's help it. return cast(Union[bool, Error], self._prepared) @property def misconfigured(self) -> bool: """Is plugin misconfigured?""" return isinstance(self._prepared, errors.MisconfigurationError) @property def problem(self) -> Optional[Exception]: """Return the Exception raised during plugin setup, or None if all is well""" if isinstance(self._prepared, Exception): return self._prepared return None @property def available(self) -> bool: """Is plugin available, i.e. prepared or misconfigured?""" return self._prepared is True or self.misconfigured def __repr__(self) -> str: return "PluginEntryPoint#{0}".format(self.name) def __str__(self) -> str: lines = [ "* {0}".format(self.name), "Description: {0}".format(self.plugin_cls.description), "Interfaces: {0}".format(", ".join( iface.__name__ for iface in PLUGIN_INTERFACES if issubclass(self.plugin_cls, iface) )), "Entry point: {0}".format(self.entry_point), ] if self.initialized: lines.append("Initialized: {0}".format(self.init())) if self.prepared: lines.append("Prep: {0}".format(self.prepare())) return "\n".join(lines) class PluginsRegistry(Mapping): """Plugins registry.""" def __init__(self, plugins: Mapping[str, PluginEntryPoint]) -> None: # plugins are sorted so the same order is used between runs. # This prevents deadlock caused by plugins acquiring a lock # and ensures at least one concurrent Certbot instance will run # successfully. self._plugins = dict(sorted(plugins.items())) @classmethod def find_all(cls) -> 'PluginsRegistry': """Find plugins using setuptools entry points.""" plugins: Dict[str, PluginEntryPoint] = {} plugin_paths_string = os.getenv('CERTBOT_PLUGIN_PATH') plugin_paths = plugin_paths_string.split(':') if plugin_paths_string else [] # XXX should ensure this only happens once sys.path.extend(plugin_paths) entry_points = list(importlib_metadata.entry_points( # pylint: disable=unexpected-keyword-arg group=constants.SETUPTOOLS_PLUGINS_ENTRY_POINT)) old_entry_points = list(importlib_metadata.entry_points( # pylint: disable=unexpected-keyword-arg group=constants.OLD_SETUPTOOLS_PLUGINS_ENTRY_POINT)) for entry_point in entry_points + old_entry_points: try: cls._load_entry_point(entry_point, plugins) except Exception as e: raise errors.PluginError( f"The '{entry_point.module}' plugin errored while loading: {e}. " "You may need to remove or update this plugin. The Certbot log will " "contain the full error details and this should be reported to the " "plugin developer.") from e return cls(plugins) @classmethod def _load_entry_point(cls, entry_point: importlib_metadata.EntryPoint, plugins: Dict[str, PluginEntryPoint]) -> None: plugin_ep = PluginEntryPoint(entry_point) if plugin_ep.name in plugins: other_ep = plugins[plugin_ep.name] plugin1_dist = plugin_ep.entry_point.dist plugin2_dist = other_ep.entry_point.dist plugin1 = plugin1_dist.name.lower() if plugin1_dist else "unknown" plugin2 = plugin2_dist.name.lower() if plugin2_dist else "unknown" # pylint: disable=broad-exception-raised raise Exception("Duplicate plugin name {0} from {1} and {2}.".format( plugin_ep.name, plugin1, plugin2)) if issubclass(plugin_ep.plugin_cls, interfaces.Plugin): plugins[plugin_ep.name] = plugin_ep else: # pragma: no cover logger.warning( "%r does not inherit from Plugin, skipping", plugin_ep) def __getitem__(self, name: str) -> PluginEntryPoint: return self._plugins[name] def __iter__(self) -> Iterator[str]: return iter(self._plugins) def __len__(self) -> int: return len(self._plugins) def init(self, config: configuration.NamespaceConfig) -> List[interfaces.Plugin]: """Initialize all plugins in the registry.""" return [plugin_ep.init(config) for plugin_ep in self._plugins.values()] def filter(self, pred: Callable[[PluginEntryPoint], bool]) -> "PluginsRegistry": """Filter plugins based on predicate.""" return type(self)({name: plugin_ep for name, plugin_ep in self._plugins.items() if pred(plugin_ep)}) def visible(self) -> "PluginsRegistry": """Filter plugins based on visibility.""" return self.filter(lambda plugin_ep: not plugin_ep.hidden) def ifaces(self, *ifaces_groups: Iterable[Type]) -> "PluginsRegistry": """Filter plugins based on interfaces.""" return self.filter(lambda p_ep: p_ep.ifaces(*ifaces_groups)) def prepare(self) -> List[Union[bool, Error]]: """Prepare all plugins in the registry.""" return [plugin_ep.prepare() for plugin_ep in self._plugins.values()] def available(self) -> "PluginsRegistry": """Filter plugins based on availability.""" return self.filter(lambda p_ep: p_ep.available) # successfully prepared + misconfigured def find_init(self, plugin: interfaces.Plugin) -> Optional[PluginEntryPoint]: """Find an initialized plugin. This is particularly useful for finding a name for the plugin:: # plugin is an instance providing Plugin, initialized # somewhere else in the code plugin_registry.find_init(plugin).name Returns ``None`` if ``plugin`` is not found in the registry. """ # use list instead of set because PluginEntryPoint is not hashable candidates = [plugin_ep for plugin_ep in self._plugins.values() if plugin_ep.initialized and plugin_ep.init() is plugin] assert len(candidates) <= 1 if candidates: return candidates[0] return None def __repr__(self) -> str: return "{0}({1})".format( self.__class__.__name__, ','.join( repr(p_ep) for p_ep in self._plugins.values())) def __str__(self) -> str: if not self._plugins: return "No plugins" return "\n\n".join(str(p_ep) for p_ep in self._plugins.values()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/plugins/manual.py0000664000175100017510000002727614561227515021133 0ustar00ericaerica"""Manual authenticator plugin""" import logging from typing import Any from typing import Callable from typing import Dict from typing import Iterable from typing import List from typing import Tuple from typing import Type from acme import challenges from certbot import achallenges from certbot import errors from certbot import interfaces from certbot import reverter from certbot import util from certbot._internal import hooks from certbot._internal.cli import cli_constants from certbot.compat import misc from certbot.compat import os from certbot.display import ops as display_ops from certbot.display import util as display_util from certbot.plugins import common logger = logging.getLogger(__name__) class Authenticator(common.Plugin, interfaces.Authenticator): """Manual authenticator This plugin allows the user to perform the domain validation challenge(s) themselves. This either be done manually by the user or through shell scripts provided to Certbot. """ description = 'Manual configuration or run your own shell scripts' hidden = True long_description = ( 'Authenticate through manual configuration or custom shell scripts. ' 'When using shell scripts, an authenticator script must be provided. ' 'The environment variables available to this script depend on the ' 'type of challenge. $CERTBOT_DOMAIN will always contain the domain ' 'being authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION ' 'is the validation string, and $CERTBOT_TOKEN is the filename of the ' 'resource requested when performing an HTTP-01 challenge. An additional ' 'cleanup script can also be provided and can use the additional variable ' '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script. ' 'For both authenticator and cleanup script, on HTTP-01 and DNS-01 challenges, ' '$CERTBOT_REMAINING_CHALLENGES will be equal to the number of challenges that ' 'remain after the current one, and $CERTBOT_ALL_DOMAINS contains a comma-separated ' 'list of all domains that are challenged for the current certificate.') # Include the full stop at the end of the FQDN in the instructions below for the null # label of the DNS root, as stated in section 3.1 of RFC 1035. While not necessary # for most day to day usage of hostnames, when adding FQDNs to a DNS zone editor, this # full stop is often mandatory. Without a full stop, the entered name is often seen as # relative to the DNS zone origin, which could lead to entries for, e.g.: # _acme-challenge.example.com.example.com. For users unaware of this subtle detail, # including the trailing full stop in the DNS instructions below might avert this issue. _DNS_INSTRUCTIONS = """\ Please deploy a DNS TXT record under the name: {domain}. with the following value: {validation} """ _DNS_VERIFY_INSTRUCTIONS = """ Before continuing, verify the TXT record has been deployed. Depending on the DNS provider, this may take some time, from a few seconds to multiple minutes. You can check if it has finished deploying with aid of online tools, such as the Google Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/{domain}. Look for one or more bolded line(s) below the line ';ANSWER'. It should show the value(s) you've just added. """ _HTTP_INSTRUCTIONS = """\ Create a file containing just this data: {validation} And make it available on your web server at this URL: {uri} """ _SUBSEQUENT_CHALLENGE_INSTRUCTIONS = """ (This must be set up in addition to the previous challenges; do not remove, replace, or undo the previous challenge tasks yet.) """ _SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS = """ (This must be set up in addition to the previous challenges; do not remove, replace, or undo the previous challenge tasks yet. Note that you might be asked to create multiple distinct TXT records with the same name. This is permitted by DNS standards.) """ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.reverter = reverter.Reverter(self.config) self.reverter.recovery_routine() self.env: Dict[achallenges.AnnotatedChallenge, Dict[str, str]] = {} self.subsequent_dns_challenge = False self.subsequent_any_challenge = False @classmethod def add_parser_arguments(cls, add: Callable[..., None]) -> None: add('auth-hook', help='Path or command to execute for the authentication script') add('cleanup-hook', help='Path or command to execute for the cleanup script') util.add_deprecated_argument(add, 'public-ip-logging-ok', 0) def prepare(self) -> None: # pylint: disable=missing-function-docstring if self.config.noninteractive_mode and not self.conf('auth-hook'): raise errors.PluginError( 'An authentication script must be provided with --{0} when ' 'using the manual plugin non-interactively.'.format( self.option_name('auth-hook'))) self._validate_hooks() def _validate_hooks(self) -> None: if self.config.validate_hooks: for name in ('auth-hook', 'cleanup-hook'): hook = self.conf(name) if hook is not None: hook_prefix = self.option_name(name)[:-len('-hook')] hooks.validate_hook(hook, hook_prefix) def more_info(self) -> str: # pylint: disable=missing-function-docstring return ( 'This plugin allows the user to customize setup for domain ' 'validation challenges either through shell scripts provided by ' 'the user or by performing the setup manually.') def auth_hint(self, failed_achalls: Iterable[achallenges.AnnotatedChallenge]) -> str: def has_chall(cls: Type[challenges.Challenge]) -> bool: return any(isinstance(achall.chall, cls) for achall in failed_achalls) has_dns = has_chall(challenges.DNS01) resource_names = { challenges.DNS01: 'DNS TXT records', challenges.HTTP01: 'challenge files', challenges.TLSALPN01: 'TLS-ALPN certificates' } resources = ' and '.join(sorted([v for k, v in resource_names.items() if has_chall(k)])) if self.conf('auth-hook'): return ( 'The Certificate Authority failed to verify the {resources} created by the ' '--manual-auth-hook. Ensure that this hook is functioning correctly{dns_hint}. ' 'Refer to "{certbot} --help manual" and the Certbot User Guide.' .format( certbot=cli_constants.cli_command, resources=resources, dns_hint=( ' and that it waits a sufficient duration of time for DNS propagation' ) if has_dns else '' ) ) else: return ( 'The Certificate Authority failed to verify the manually created {resources}. ' 'Ensure that you created these in the correct location{dns_hint}.' .format( resources=resources, dns_hint=( ', or try waiting longer for DNS propagation on the next attempt' ) if has_dns else '' ) ) def get_chall_pref(self, domain: str) -> Iterable[Type[challenges.Challenge]]: # pylint: disable=unused-argument,missing-function-docstring return [challenges.HTTP01, challenges.DNS01] def perform(self, achalls: List[achallenges.AnnotatedChallenge] ) -> List[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring responses = [] last_dns_achall = 0 for i, achall in enumerate(achalls): if isinstance(achall.chall, challenges.DNS01): last_dns_achall = i for i, achall in enumerate(achalls): if self.conf('auth-hook'): self._perform_achall_with_script(achall, achalls) else: self._perform_achall_manually(achall, i == last_dns_achall) responses.append(achall.response(achall.account_key)) return responses def _perform_achall_with_script(self, achall: achallenges.AnnotatedChallenge, achalls: List[achallenges.AnnotatedChallenge]) -> None: env = { "CERTBOT_DOMAIN": achall.domain, "CERTBOT_VALIDATION": achall.validation(achall.account_key), "CERTBOT_ALL_DOMAINS": ','.join(one_achall.domain for one_achall in achalls), "CERTBOT_REMAINING_CHALLENGES": str(len(achalls) - achalls.index(achall) - 1), } if isinstance(achall.chall, challenges.HTTP01): env['CERTBOT_TOKEN'] = achall.chall.encode('token') else: os.environ.pop('CERTBOT_TOKEN', None) os.environ.update(env) _, out = self._execute_hook('auth-hook', achall.domain) env['CERTBOT_AUTH_OUTPUT'] = out.strip() self.env[achall] = env def _perform_achall_manually(self, achall: achallenges.AnnotatedChallenge, last_dns_achall: bool = False) -> None: validation = achall.validation(achall.account_key) if isinstance(achall.chall, challenges.HTTP01): msg = self._HTTP_INSTRUCTIONS.format( achall=achall, encoded_token=achall.chall.encode('token'), port=self.config.http01_port, uri=achall.chall.uri(achall.domain), validation=validation) else: assert isinstance(achall.chall, challenges.DNS01) msg = self._DNS_INSTRUCTIONS.format( domain=achall.validation_domain_name(achall.domain), validation=validation) if isinstance(achall.chall, challenges.DNS01): if self.subsequent_dns_challenge: # 2nd or later dns-01 challenge msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS elif self.subsequent_any_challenge: # 1st dns-01 challenge, but 2nd or later *any* challenge, so # instruct user not to remove any previous http-01 challenge msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS self.subsequent_dns_challenge = True if last_dns_achall: # last dns-01 challenge msg += self._DNS_VERIFY_INSTRUCTIONS.format( domain=achall.validation_domain_name(achall.domain)) elif self.subsequent_any_challenge: # 2nd or later challenge of another type msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS display_util.notification(msg, wrap=False, force_interactive=True) self.subsequent_any_challenge = True def cleanup(self, achalls: Iterable[achallenges.AnnotatedChallenge]) -> None: # pylint: disable=missing-function-docstring if self.conf('cleanup-hook'): for achall in achalls: env = self.env.pop(achall) if 'CERTBOT_TOKEN' not in env: os.environ.pop('CERTBOT_TOKEN', None) os.environ.update(env) self._execute_hook('cleanup-hook', achall.domain) self.reverter.recovery_routine() def _execute_hook(self, hook_name: str, achall_domain: str) -> Tuple[str, str]: returncode, err, out = misc.execute_command_status( self.option_name(hook_name), self.conf(hook_name), env=util.env_no_snap_for_external_calls() ) display_ops.report_executed_command( f"Hook '--manual-{hook_name}' for {achall_domain}", returncode, out, err) return err, out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/plugins/null.py0000664000175100017510000000305314561227515020613 0ustar00ericaerica"""Null plugin.""" import logging from typing import Callable from typing import List from typing import Optional from typing import Union from certbot import interfaces from certbot.plugins import common logger = logging.getLogger(__name__) class Installer(common.Plugin, interfaces.Installer): """Null installer.""" description = "Null Installer" hidden = True @classmethod def add_parser_arguments(cls, add: Callable[..., None]) -> None: pass # pylint: disable=missing-function-docstring def prepare(self) -> None: pass # pragma: no cover def more_info(self) -> str: return "Installer that doesn't do anything (for testing)." def get_all_names(self) -> List[str]: return [] def deploy_cert(self, domain: str, cert_path: str, key_path: str, chain_path: str, fullchain_path: str) -> None: pass # pragma: no cover def enhance(self, domain: str, enhancement: str, options: Optional[Union[List[str], str]] = None) -> None: pass # pragma: no cover def supported_enhancements(self) -> List[str]: return [] def save(self, title: Optional[str] = None, temporary: bool = False) -> None: pass # pragma: no cover def rollback_checkpoints(self, rollback: int = 1) -> None: pass # pragma: no cover def recovery_routine(self) -> None: pass # pragma: no cover def config_test(self) -> None: pass # pragma: no cover def restart(self) -> None: pass # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/plugins/selection.py0000664000175100017510000003663414561227515021641 0ustar00ericaerica"""Decide which plugins to use for authentication & installation""" import logging from typing import cast from typing import Iterable from typing import List from typing import Optional from typing import Tuple from typing import Type from typing import TypeVar from certbot import configuration from certbot import errors from certbot import interfaces from certbot._internal.plugins import disco from certbot.compat import os from certbot.display import util as display_util logger = logging.getLogger(__name__) def pick_configurator(config: configuration.NamespaceConfig, default: Optional[str], plugins: disco.PluginsRegistry, question: str = "How would you like to authenticate and install " "certificates?") -> Optional[interfaces.Plugin]: """Pick configurator plugin.""" return pick_plugin( config, default, plugins, question, (interfaces.Authenticator, interfaces.Installer)) def pick_installer(config: configuration.NamespaceConfig, default: Optional[str], plugins: disco.PluginsRegistry, question: str = "How would you like to install certificates?" ) -> Optional[interfaces.Installer]: """Pick installer plugin.""" return pick_plugin(config, default, plugins, question, (interfaces.Installer,)) def pick_authenticator(config: configuration.NamespaceConfig, default: Optional[str], plugins: disco.PluginsRegistry, question: str = "How would you " "like to authenticate with the ACME CA?" ) -> Optional[interfaces.Authenticator]: """Pick authentication plugin.""" return pick_plugin( config, default, plugins, question, (interfaces.Authenticator,)) def get_unprepared_installer(config: configuration.NamespaceConfig, plugins: disco.PluginsRegistry) -> Optional[interfaces.Installer]: """ Get an unprepared interfaces.Installer object. :param certbot.configuration.NamespaceConfig config: Configuration :param certbot._internal.plugins.disco.PluginsRegistry plugins: All plugins registered as entry points. :returns: Unprepared installer plugin or None :rtype: Plugin or None """ _, req_inst = cli_plugin_requests(config) if not req_inst: return None installers = plugins.filter(lambda p_ep: p_ep.check_name(req_inst)) installers.init(config) if len(installers) > 1: raise errors.PluginSelectionError( "Found multiple installers with the name %s, Certbot is unable to " "determine which one to use. Skipping." % req_inst) if installers: inst = list(installers.values())[0] logger.debug("Selecting plugin: %s", inst) return inst.init(config) raise errors.PluginSelectionError( "Could not select or initialize the requested installer %s." % req_inst) P = TypeVar('P', bound=interfaces.Plugin) def pick_plugin(config: configuration.NamespaceConfig, default: Optional[str], plugins: disco.PluginsRegistry, question: str, ifaces: Iterable[Type]) -> Optional[P]: """Pick plugin. :param certbot.configuration.NamespaceConfig config: Configuration :param str default: Plugin name supplied by user or ``None``. :param certbot._internal.plugins.disco.PluginsRegistry plugins: All plugins registered as entry points. :param str question: Question to be presented to the user in case multiple candidates are found. :param list ifaces: Interfaces that plugins must provide. :returns: Initialized plugin. :rtype: Plugin """ if default is not None: # throw more UX-friendly error if default not in plugins filtered = plugins.filter(lambda p_ep: p_ep.check_name(default)) else: if config.noninteractive_mode: # it's really bad to auto-select the single available plugin in # non-interactive mode, because an update could later add a second # available plugin raise errors.MissingCommandlineFlag( "Missing command line flags. For non-interactive execution, " "you will need to specify a plugin on the command line. Run " "with '--help plugins' to see a list of options, and see " "https://eff.org/letsencrypt-plugins for more detail on what " "the plugins do and how to use them.") filtered = plugins.visible() filtered = filtered.ifaces(ifaces) filtered.init(config) filtered.prepare() prepared = filtered.available() if len(prepared) > 1: logger.debug("Multiple candidate plugins: %s", prepared) plugin_ep1 = choose_plugin(list(prepared.values()), question) if plugin_ep1 is None: return None return cast(P, plugin_ep1.init()) elif len(prepared) == 1: plugin_ep2 = list(prepared.values())[0] logger.debug("Single candidate plugin: %s", plugin_ep2) if plugin_ep2.misconfigured: return None return plugin_ep2.init() else: logger.debug("No candidate plugin") return None def choose_plugin(prepared: List[disco.PluginEntryPoint], question: str) -> Optional[disco.PluginEntryPoint]: """Allow the user to choose their plugin. :param list prepared: List of `~.PluginEntryPoint`. :param str question: Question to be presented to the user. :returns: Plugin entry point chosen by the user. :rtype: `~.PluginEntryPoint` """ opts = [plugin_ep.description_with_name + (" [Misconfigured]" if plugin_ep.misconfigured else "") for plugin_ep in prepared] while True: code, index = display_util.menu(question, opts, force_interactive=True) if code == display_util.OK: plugin_ep = prepared[index] if plugin_ep.misconfigured: display_util.notification( "The selected plugin encountered an error while parsing " "your server configuration and cannot be used. The error " "was:\n\n{0}".format(plugin_ep.prepare()), pause=False) else: return plugin_ep else: return None noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-gehirn", "dns-google", "dns-linode", "dns-luadns", "dns-nsone", "dns-ovh", "dns-rfc2136", "dns-route53", "dns-sakuracloud"] def record_chosen_plugins(config: configuration.NamespaceConfig, plugins: disco.PluginsRegistry, auth: Optional[interfaces.Authenticator], inst: Optional[interfaces.Installer]) -> None: """Update the config entries to reflect the plugins we actually selected.""" config.authenticator = None if auth: auth_ep = plugins.find_init(auth) if auth_ep: config.authenticator = auth_ep.name config.installer = None if inst: inst_ep = plugins.find_init(inst) if inst_ep: config.installer = inst_ep.name logger.info("Plugins selected: Authenticator %s, Installer %s", config.authenticator, config.installer) def choose_configurator_plugins(config: configuration.NamespaceConfig, plugins: disco.PluginsRegistry, verb: str) -> Tuple[Optional[interfaces.Installer], Optional[interfaces.Authenticator]]: """ Figure out which configurator we're going to use, modifies config.authenticator and config.installer strings to reflect that choice if necessary. :raises errors.PluginSelectionError if there was a problem :returns: tuple of (`Installer` or None, `Authenticator` or None) :rtype: tuple """ req_auth, req_inst = cli_plugin_requests(config) installer_question = "" if verb == "enhance": installer_question = ("Which installer would you like to use to " "configure the selected enhancements?") # Which plugins do we need? if verb == "run": need_inst = need_auth = True from certbot._internal.cli import cli_command if req_auth in noninstaller_plugins and not req_inst: msg = ('With the {0} plugin, you probably want to use the "certonly" command, eg:{1}' '{1} {2} certonly --{0}{1}{1}' '(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins' '{1} and "--help plugins" for more information.)'.format( req_auth, os.linesep, cli_command)) raise errors.MissingCommandlineFlag(msg) else: need_inst = need_auth = False if verb == "certonly": need_auth = True elif verb in ("install", "enhance"): need_inst = True if config.authenticator: logger.warning("Specifying an authenticator doesn't make sense when " "running Certbot with verb \"%s\"", verb) # Try to meet the user's request and/or ask them to pick plugins authenticator: Optional[interfaces.Authenticator] = None installer: Optional[interfaces.Installer] = None if verb == "run" and req_auth == req_inst: # Unless the user has explicitly asked for different auth/install, # only consider offering a single choice configurator = pick_configurator(config, req_inst, plugins) authenticator = cast(Optional[interfaces.Authenticator], configurator) installer = cast(Optional[interfaces.Installer], configurator) else: if need_inst or req_inst: installer = pick_installer(config, req_inst, plugins, installer_question) if need_auth: authenticator = pick_authenticator(config, req_auth, plugins) # Report on any failures if need_inst and not installer: diagnose_configurator_problem("installer", req_inst, plugins) if need_auth and not authenticator: diagnose_configurator_problem("authenticator", req_auth, plugins) # As a special case for certonly, if a user selected apache or nginx, set # the relevant installer (unless the user specifically specified no # installer or only specified an authenticator on the command line) if verb == "certonly" and authenticator is not None: # user specified --nginx or --apache on CLI selected_configurator = config.nginx or config.apache # user didn't request an authenticator, and so interactively chose nginx # or apache interactively_selected = req_auth is None and authenticator.name in ("nginx", "apache") if selected_configurator or interactively_selected: installer = cast(Optional[interfaces.Installer], authenticator) logger.debug("Selected authenticator %s and installer %s", authenticator, installer) record_chosen_plugins(config, plugins, authenticator, installer) return installer, authenticator def set_configurator(previously: Optional[str], now: Optional[str]) -> Optional[str]: """ Setting configurators multiple ways is okay, as long as they all agree :param str previously: previously identified request for the installer/authenticator :param str now: the request currently being processed """ if not now: # we're not actually setting anything return previously if previously: if previously != now: msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}" raise errors.PluginSelectionError(msg.format(repr(previously), repr(now))) return now def cli_plugin_requests(config: configuration.NamespaceConfig ) -> Tuple[Optional[str], Optional[str]]: """ Figure out which plugins the user requested with CLI and config options :returns: (requested authenticator string or None, requested installer string or None) :rtype: tuple """ req_inst = req_auth = config.configurator req_inst = set_configurator(req_inst, config.installer) req_auth = set_configurator(req_auth, config.authenticator) if config.nginx: req_inst = set_configurator(req_inst, "nginx") req_auth = set_configurator(req_auth, "nginx") if config.apache: req_inst = set_configurator(req_inst, "apache") req_auth = set_configurator(req_auth, "apache") if config.standalone: req_auth = set_configurator(req_auth, "standalone") if config.webroot: req_auth = set_configurator(req_auth, "webroot") if config.manual: req_auth = set_configurator(req_auth, "manual") if config.dns_cloudflare: req_auth = set_configurator(req_auth, "dns-cloudflare") if config.dns_digitalocean: req_auth = set_configurator(req_auth, "dns-digitalocean") if config.dns_dnsimple: req_auth = set_configurator(req_auth, "dns-dnsimple") if config.dns_dnsmadeeasy: req_auth = set_configurator(req_auth, "dns-dnsmadeeasy") if config.dns_gehirn: req_auth = set_configurator(req_auth, "dns-gehirn") if config.dns_google: req_auth = set_configurator(req_auth, "dns-google") if config.dns_linode: req_auth = set_configurator(req_auth, "dns-linode") if config.dns_luadns: req_auth = set_configurator(req_auth, "dns-luadns") if config.dns_nsone: req_auth = set_configurator(req_auth, "dns-nsone") if config.dns_ovh: req_auth = set_configurator(req_auth, "dns-ovh") if config.dns_rfc2136: req_auth = set_configurator(req_auth, "dns-rfc2136") if config.dns_route53: req_auth = set_configurator(req_auth, "dns-route53") if config.dns_sakuracloud: req_auth = set_configurator(req_auth, "dns-sakuracloud") logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) return req_auth, req_inst def diagnose_configurator_problem(cfg_type: str, requested: Optional[str], plugins: disco.PluginsRegistry) -> None: """ Raise the most helpful error message about a plugin being unavailable :param str cfg_type: either "installer" or "authenticator" :param str requested: the plugin that was requested :param .PluginsRegistry plugins: available plugins :raises error.PluginSelectionError: if there was a problem """ if requested: if requested not in plugins: msg = "The requested {0} plugin does not appear to be installed".format(requested) else: msg = ("The {0} plugin is not working; there may be problems with " "your existing configuration.\nThe error was: {1!r}" .format(requested, plugins[requested].problem)) elif cfg_type == "installer": from certbot._internal.cli import cli_command msg = ('Certbot doesn\'t know how to automatically configure the web ' 'server on this system. However, it can still get a certificate for ' 'you. Please run "{0} certonly" to do so. You\'ll need to ' 'manually configure your web server to use the resulting ' 'certificate.').format(cli_command) else: msg = "{0} could not be determined or is not installed".format(cfg_type) raise errors.PluginSelectionError(msg) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/plugins/standalone.py0000664000175100017510000002217414561227515021776 0ustar00ericaerica"""Standalone Authenticator.""" import collections import errno import logging import socket from typing import Any from typing import Callable from typing import DefaultDict from typing import Dict from typing import Iterable from typing import List from typing import Mapping from typing import Set from typing import Tuple from typing import Type from typing import TYPE_CHECKING from OpenSSL import crypto from acme import challenges from acme import standalone as acme_standalone from certbot import achallenges from certbot import errors from certbot import interfaces from certbot.display import util as display_util from certbot.plugins import common logger = logging.getLogger(__name__) if TYPE_CHECKING: ServedType = DefaultDict[ acme_standalone.BaseDualNetworkedServers, Set[achallenges.AnnotatedChallenge] ] class ServerManager: """Standalone servers manager. Manager for `ACMEServer` and `ACMETLSServer` instances. `certs` and `http_01_resources` correspond to `acme.crypto_util.SSLSocket.certs` and `acme.crypto_util.SSLSocket.http_01_resources` respectively. All created servers share the same certificates and resources, so if you're running both TLS and non-TLS instances, HTTP01 handlers will serve the same URLs! """ def __init__(self, certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]], http_01_resources: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] ) -> None: self._instances: Dict[int, acme_standalone.HTTP01DualNetworkedServers] = {} self.certs = certs self.http_01_resources = http_01_resources def run(self, port: int, challenge_type: Type[challenges.Challenge], listenaddr: str = "") -> acme_standalone.HTTP01DualNetworkedServers: """Run ACME server on specified ``port``. This method is idempotent, i.e. all calls with the same pair of ``(port, challenge_type)`` will reuse the same server. :param int port: Port to run the server on. :param challenge_type: Subclass of `acme.challenges.Challenge`, currently only `acme.challenge.HTTP01`. :param str listenaddr: (optional) The address to listen on. Defaults to all addrs. :returns: DualNetworkedServers instance. :rtype: ACMEServerMixin """ assert challenge_type == challenges.HTTP01 if port in self._instances: return self._instances[port] address = (listenaddr, port) try: servers = acme_standalone.HTTP01DualNetworkedServers( address, self.http_01_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) servers.serve_forever() # if port == 0, then random free port on OS is taken # both servers, if they exist, have the same port real_port = servers.getsocknames()[0][1] self._instances[real_port] = servers return servers def stop(self, port: int) -> None: """Stop ACME server running on the specified ``port``. :param int port: """ instance = self._instances[port] for sockname in instance.getsocknames(): logger.debug("Stopping server at %s:%d...", *sockname[:2]) instance.shutdown_and_server_close() del self._instances[port] def running(self) -> Dict[int, acme_standalone.HTTP01DualNetworkedServers]: """Return all running instances. Once the server is stopped using `stop`, it will not be returned. :returns: Mapping from ``port`` to ``servers``. :rtype: tuple """ return self._instances.copy() class Authenticator(common.Plugin, interfaces.Authenticator): """Standalone Authenticator. This authenticator creates its own ephemeral TCP listener on the necessary port in order to respond to incoming http-01 challenges from the certificate authority. Therefore, it does not rely on any existing server program. """ description = """Runs an HTTP server locally which serves the necessary validation files \ under the /.well-known/acme-challenge/ request path. Suitable if there is no HTTP server already \ running. HTTP challenge only (wildcards not supported).""" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.served: ServedType = collections.defaultdict(set) # Stuff below is shared across threads (i.e. servers read # values, main thread writes). Due to the nature of CPython's # GIL, the operations are safe, c.f. # https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe self.certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]] = {} self.http_01_resources: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] = set() self.servers = ServerManager(self.certs, self.http_01_resources) @classmethod def add_parser_arguments(cls, add: Callable[..., None]) -> None: pass # No additional argument for the standalone plugin parser def more_info(self) -> str: # pylint: disable=missing-function-docstring return("This authenticator creates its own ephemeral TCP listener " "on the necessary port in order to respond to incoming " "http-01 challenges from the certificate authority. Therefore, " "it does not rely on any existing server program.") def prepare(self) -> None: # pylint: disable=missing-function-docstring pass def get_chall_pref(self, domain: str) -> Iterable[Type[challenges.Challenge]]: # pylint: disable=unused-argument,missing-function-docstring return [challenges.HTTP01] def perform(self, achalls: Iterable[achallenges.AnnotatedChallenge] ) -> List[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring return [self._try_perform_single(achall) for achall in achalls] def _try_perform_single(self, achall: achallenges.AnnotatedChallenge) -> challenges.ChallengeResponse: while True: try: return self._perform_single(achall) except errors.StandaloneBindError as error: _handle_perform_error(error) def _perform_single(self, achall: achallenges.AnnotatedChallenge) -> challenges.ChallengeResponse: servers, response = self._perform_http_01(achall) self.served[servers].add(achall) return response def _perform_http_01(self, achall: achallenges.AnnotatedChallenge ) -> Tuple[acme_standalone.HTTP01DualNetworkedServers, challenges.ChallengeResponse]: port = self.config.http01_port addr = self.config.http01_address servers = self.servers.run(port, challenges.HTTP01, listenaddr=addr) response, validation = achall.response_and_validation() resource = acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation) self.http_01_resources.add(resource) return servers, response def cleanup(self, achalls: Iterable[achallenges.AnnotatedChallenge]) -> None: # pylint: disable=missing-function-docstring # reduce self.served and close servers if no challenges are served for unused_servers, server_achalls in self.served.items(): for achall in achalls: if achall in server_achalls: server_achalls.remove(achall) for port, servers in self.servers.running().items(): if not self.served[servers]: self.servers.stop(port) def auth_hint(self, failed_achalls: List[achallenges.AnnotatedChallenge]) -> str: port, addr = self.config.http01_port, self.config.http01_address neat_addr = f"{addr}:{port}" if addr else f"port {port}" return ("The Certificate Authority failed to download the challenge files from " f"the temporary standalone webserver started by Certbot on {neat_addr}. " "Ensure that the listed domains point to this machine and that it can " "accept inbound connections from the internet.") def _handle_perform_error(error: errors.StandaloneBindError) -> None: if error.socket_error.errno == errno.EACCES: raise errors.PluginError( "Could not bind TCP port {0} because you don't have " "the appropriate permissions (for example, you " "aren't running this program as " "root).".format(error.port)) if error.socket_error.errno == errno.EADDRINUSE: msg = ( "Could not bind TCP port {0} because it is already in " "use by another process on this system (such as a web " "server). Please stop the program in question and " "then try again.".format(error.port)) should_retry = display_util.yesno(msg, "Retry", "Cancel", default=False) if not should_retry: raise errors.PluginError(msg) else: raise error ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/plugins/webroot.py0000664000175100017510000003716014561227515021330 0ustar00ericaerica"""Webroot plugin.""" import argparse import collections import json import logging from typing import Any from typing import Callable from typing import DefaultDict from typing import Dict from typing import Iterable from typing import List from typing import Optional from typing import Sequence from typing import Set from typing import Type from typing import Union from acme import challenges from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot._internal import cli from certbot.achallenges import AnnotatedChallenge from certbot.compat import filesystem from certbot.compat import os from certbot.display import ops from certbot.display import util as display_util from certbot.plugins import common from certbot.plugins import util from certbot.util import safe_open logger = logging.getLogger(__name__) _WEB_CONFIG_CONTENT = """\ """ # This list references the hashes of all versions of the web.config files that Certbot could # have generated during an HTTP-01 challenge. If you modify _WEB_CONFIG_CONTENT, you MUST add # the new hash in this list. _WEB_CONFIG_SHA256SUMS = [ "20c5ca1bd58fa8ad5f07a2f1be8b7cbb707c20fcb607a8fc8db9393952846a97", "8d31383d3a079d2098a9d0c0921f4ab87e708b9868dc3f314d54094c2fe70336" ] class Authenticator(common.Plugin, interfaces.Authenticator): """Webroot Authenticator.""" description = """\ Saves the necessary validation files to a .well-known/acme-challenge/ directory within the \ nominated webroot path. A seperate HTTP server must be running and serving files from the \ webroot path. HTTP challenge only (wildcards not supported).""" MORE_INFO = """\ Authenticator plugin that performs http-01 challenge by saving necessary validation resources to appropriate paths on the file system. It expects that there is some other HTTP server configured to serve all files under specified web root ({0}).""" def more_info(self) -> str: # pylint: disable=missing-function-docstring return self.MORE_INFO.format(self.conf("path")) @classmethod def add_parser_arguments(cls, add: Callable[..., None]) -> None: add("path", "-w", default=[], action=_WebrootPathAction, help="public_html / webroot path. This can be specified multiple " "times to handle different domains; each domain will have " "the webroot path that preceded it. For instance: `-w " "/var/www/example -d example.com -d www.example.com -w " "/var/www/thing -d thing.net -d m.thing.net` (default: Ask)") add("map", default={}, action=_WebrootMapAction, help="JSON dictionary mapping domains to webroot paths; this " "implies -d for each entry. You may need to escape this from " "your shell. E.g.: --webroot-map " '\'{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}\' ' "This option is merged with, but takes precedence over, -w / " "-d entries. At present, if you put webroot-map in a config " "file, it needs to be on a single line, like: webroot-map = " '{"example.com":"/var/www"}.') def auth_hint(self, failed_achalls: List[AnnotatedChallenge]) -> str: # pragma: no cover return ("The Certificate Authority failed to download the temporary challenge files " "created by Certbot. Ensure that the listed domains serve their content from " "the provided --webroot-path/-w and that files created there can be downloaded " "from the internet.") def get_chall_pref(self, domain: str) -> Iterable[Type[challenges.Challenge]]: # pylint: disable=unused-argument,missing-function-docstring return [challenges.HTTP01] def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.full_roots: Dict[str, str] = {} self.performed: DefaultDict[str, Set[AnnotatedChallenge]] = collections.defaultdict(set) # stack of dirs successfully created by this authenticator self._created_dirs: List[str] = [] def prepare(self) -> None: # pylint: disable=missing-function-docstring pass def perform(self, achalls: List[AnnotatedChallenge]) -> List[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring self._set_webroots(achalls) self._create_challenge_dirs() return [self._perform_single(achall) for achall in achalls] def _set_webroots(self, achalls: Iterable[AnnotatedChallenge]) -> None: if self.conf("path"): webroot_path = self.conf("path")[-1] logger.info("Using the webroot path %s for all unmatched domains.", webroot_path) for achall in achalls: self.conf("map").setdefault(achall.domain, webroot_path) else: known_webroots = list(set(self.conf("map").values())) for achall in achalls: if achall.domain not in self.conf("map"): new_webroot = self._prompt_for_webroot(achall.domain, known_webroots) # Put the most recently input # webroot first for easy selection try: known_webroots.remove(new_webroot) except ValueError: pass known_webroots.insert(0, new_webroot) self.conf("map")[achall.domain] = new_webroot def _prompt_for_webroot(self, domain: str, known_webroots: List[str]) -> Optional[str]: webroot = None while webroot is None: if known_webroots: # Only show the menu if we have options for it webroot = self._prompt_with_webroot_list(domain, known_webroots) if webroot is None: webroot = self._prompt_for_new_webroot(domain) else: # Allow prompt to raise PluginError instead of looping forever webroot = self._prompt_for_new_webroot(domain, True) return webroot def _prompt_with_webroot_list(self, domain: str, known_webroots: List[str]) -> Optional[str]: path_flag = "--" + self.option_name("path") while True: code, index = display_util.menu( "Select the webroot for {0}:".format(domain), ["Enter a new webroot"] + known_webroots, cli_flag=path_flag, force_interactive=True) if code == display_util.CANCEL: raise errors.PluginError( "Every requested domain must have a " "webroot when using the webroot plugin.") return None if index == 0 else known_webroots[index - 1] # code == display_util.OK def _prompt_for_new_webroot(self, domain: str, allowraise: bool = False) -> Optional[str]: code, webroot = ops.validated_directory( _validate_webroot, "Input the webroot for {0}:".format(domain), force_interactive=True) if code == display_util.CANCEL: if not allowraise: return None raise errors.PluginError( "Every requested domain must have a " "webroot when using the webroot plugin.") return _validate_webroot(webroot) # code == display_util.OK def _create_challenge_dirs(self) -> None: path_map = self.conf("map") if not path_map: raise errors.PluginError( "Missing parts of webroot configuration; please set either " "--webroot-path and --domains, or --webroot-map. Run with " " --help webroot for examples.") for name, path in path_map.items(): self.full_roots[name] = os.path.join(path, os.path.normcase( challenges.HTTP01.URI_ROOT_PATH)) logger.debug("Creating root challenges validation dir at %s", self.full_roots[name]) # Change the permissions to be writable (GH #1389) # Umask is used instead of chmod to ensure the client can also # run as non-root (GH #1795) with filesystem.temp_umask(0o022): # We ignore the last prefix in the next iteration, # as it does not correspond to a folder path ('/' or 'C:') for prefix in sorted(util.get_prefixes(self.full_roots[name])[:-1], key=len): if os.path.isdir(prefix): # Don't try to create directory if it already exists, as some filesystems # won't reliably raise EEXIST or EISDIR if directory exists. continue try: # Set owner as parent directory if possible, apply mode for Linux/Windows. # For Linux, this is coupled with the "umask" call above because # os.mkdir's "mode" parameter may not always work: # https://docs.python.org/3/library/os.html#os.mkdir filesystem.mkdir(prefix, 0o755) self._created_dirs.append(prefix) try: filesystem.copy_ownership_and_apply_mode( path, prefix, 0o755, copy_user=True, copy_group=True) except (OSError, AttributeError) as exception: logger.warning("Unable to change owner and uid of webroot directory") logger.debug("Error was: %s", exception) except OSError as exception: raise errors.PluginError( "Couldn't create root for {0} http-01 " "challenge responses: {1}".format(name, exception)) # On Windows, generate a local web.config file that allows IIS to serve expose # challenge files despite the fact they do not have a file extension. if not filesystem.POSIX_MODE: web_config_path = os.path.join(self.full_roots[name], "web.config") if os.path.exists(web_config_path): logger.info("A web.config file has not been created in " "%s because another one already exists.", self.full_roots[name]) continue logger.info("Creating a web.config file in %s to allow IIS " "to serve challenge files.", self.full_roots[name]) with safe_open(web_config_path, mode="w", chmod=0o644) as web_config: web_config.write(_WEB_CONFIG_CONTENT) def _get_validation_path(self, root_path: str, achall: AnnotatedChallenge) -> str: return os.path.join(root_path, achall.chall.encode("token")) def _perform_single(self, achall: AnnotatedChallenge) -> challenges.ChallengeResponse: response, validation = achall.response_and_validation() root_path = self.full_roots[achall.domain] validation_path = self._get_validation_path(root_path, achall) logger.debug("Attempting to save validation to %s", validation_path) # Change permissions to be world-readable, owner-writable (GH #1795) with filesystem.temp_umask(0o022): with safe_open(validation_path, mode="wb", chmod=0o644) as validation_file: validation_file.write(validation.encode()) self.performed[root_path].add(achall) return response def cleanup(self, achalls: List[AnnotatedChallenge]) -> None: # pylint: disable=missing-function-docstring for achall in achalls: root_path = self.full_roots.get(achall.domain, None) if root_path is not None: validation_path = self._get_validation_path(root_path, achall) logger.debug("Removing %s", validation_path) os.remove(validation_path) self.performed[root_path].remove(achall) if not filesystem.POSIX_MODE: web_config_path = os.path.join(root_path, "web.config") if os.path.exists(web_config_path): sha256sum = crypto_util.sha256sum(web_config_path) if sha256sum in _WEB_CONFIG_SHA256SUMS: logger.info("Cleaning web.config file generated by Certbot in %s.", root_path) os.remove(web_config_path) else: logger.info("Not cleaning up the web.config file in %s " "because it is not generated by Certbot.", root_path) not_removed: List[str] = [] while self._created_dirs: path = self._created_dirs.pop() try: os.rmdir(path) except OSError as exc: not_removed.insert(0, path) logger.info("Challenge directory %s was not empty, didn't remove", path) logger.debug("Error was: %s", exc) self._created_dirs = not_removed logger.debug("All challenges cleaned up") class _WebrootMapAction(argparse.Action): """Action class for parsing webroot_map.""" def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, webroot_map: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None: if webroot_map is None: return for domains, webroot_path in json.loads(str(webroot_map)).items(): webroot_path = _validate_webroot(webroot_path) namespace.webroot_map.update( (d, webroot_path) for d in cli.add_domains(namespace, domains)) class _WebrootPathAction(argparse.Action): """Action class for parsing webroot_path.""" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._domain_before_webroot = False def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, webroot_path: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None: if webroot_path is None: return if self._domain_before_webroot: raise errors.PluginError( "If you specify multiple webroot paths, " "one of them must precede all domain flags") if namespace.webroot_path: # Apply previous webroot to all matched # domains before setting the new webroot path prev_webroot = namespace.webroot_path[-1] for domain in namespace.domains: namespace.webroot_map.setdefault(domain, prev_webroot) elif namespace.domains: self._domain_before_webroot = True namespace.webroot_path.append(_validate_webroot(str(webroot_path))) def _validate_webroot(webroot_path: str) -> str: """Validates and returns the absolute path of webroot_path. :param str webroot_path: path to the webroot directory :returns: absolute path of webroot_path :rtype: str """ if not os.path.isdir(webroot_path): raise errors.PluginError(webroot_path + " does not exist or is not a directory") return os.path.abspath(webroot_path) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/renewal.py0000664000175100017510000006306014561227515017621 0ustar00ericaerica"""Functionality for autorenewal and associated juggling of configurations""" import copy import itertools import logging import random import sys import time import traceback from typing import Any from typing import Dict from typing import Iterable from typing import List from typing import Mapping from typing import Optional from typing import Tuple from typing import Union from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import load_pem_private_key from certbot import configuration from certbot import crypto_util from certbot import errors from certbot import util from certbot._internal import cli from certbot._internal import client from certbot._internal import constants from certbot._internal import hooks from certbot._internal import storage from certbot._internal import updater from certbot._internal.display import obj as display_obj from certbot._internal.plugins import disco as plugins_disco from certbot.compat import os from certbot.display import util as display_util logger = logging.getLogger(__name__) # These are the items which get pulled out of a renewal configuration # file's renewalparams and actually used in the client configuration # during the renewal process. We have to record their types here because # the renewal configuration process loses this information. STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent", "server", "account", "authenticator", "installer", "renew_hook", "pre_hook", "post_hook", "http01_address", "preferred_chain", "key_type", "elliptic_curve"] INT_CONFIG_ITEMS = ["rsa_key_size", "http01_port"] BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names", "reuse_key", "autorenew"] CONFIG_ITEMS = set(itertools.chain( BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS, ('pref_challs',))) def reconstitute(config: configuration.NamespaceConfig, full_path: str) -> Optional[storage.RenewableCert]: """Try to instantiate a RenewableCert, updating config with relevant items. This is specifically for use in renewal and enforces several checks and policies to ensure that we can try to proceed with the renewal request. The config argument is modified by including relevant options read from the renewal configuration file. :param configuration.NamespaceConfig config: configuration for the current lineage :param str full_path: Absolute path to the configuration file that defines this lineage :returns: the RenewableCert object or None if a fatal error occurred :rtype: `storage.RenewableCert` or NoneType """ try: renewal_candidate = storage.RenewableCert(full_path, config) except (errors.CertStorageError, IOError) as error: logger.error("Renewal configuration file %s is broken.", full_path) logger.error("The error was: %s\nSkipping.", str(error)) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None if "renewalparams" not in renewal_candidate.configuration: logger.error("Renewal configuration file %s lacks " "renewalparams. Skipping.", full_path) return None renewalparams = renewal_candidate.configuration["renewalparams"] if "authenticator" not in renewalparams: logger.error("Renewal configuration file %s does not specify " "an authenticator. Skipping.", full_path) return None # Prior to Certbot v1.25.0, the default value of key_type (rsa) was not persisted to the # renewal params. If the option is absent, it means the certificate was an RSA key. # Restoring the option here is necessary to preserve the certificate key_type if # the user has upgraded directly from Certbot =v2.0.0, where the default # key_type was changed to ECDSA. See https://github.com/certbot/certbot/issues/9635. renewalparams["key_type"] = renewalparams.get("key_type", "rsa") # Now restore specific values along with their data types, if # those elements are present. renewalparams = _remove_deprecated_config_elements(renewalparams) try: restore_required_config_elements(config, renewalparams) _restore_plugin_configs(config, renewalparams) except (ValueError, errors.Error) as error: logger.error( "An error occurred while parsing %s. The error was %s. " "Skipping the file.", full_path, str(error)) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None try: config.domains = [util.enforce_domain_sanity(d) for d in renewal_candidate.names()] except errors.ConfigurationError as error: logger.error("Renewal configuration file %s references a certificate " "that contains an invalid domain name. The problem " "was: %s. Skipping.", full_path, error) return None return renewal_candidate def _restore_webroot_config(config: configuration.NamespaceConfig, renewalparams: Mapping[str, Any]) -> None: """ webroot_map is, uniquely, a dict, and the general-purpose configuration restoring logic is not able to correctly parse it from the serialized form. """ if "webroot_map" in renewalparams and not config.set_by_user("webroot_map"): config.webroot_map = renewalparams["webroot_map"] # To understand why webroot_path and webroot_map processing are not mutually exclusive, # see https://github.com/certbot/certbot/pull/7095 if "webroot_path" in renewalparams and not config.set_by_user("webroot_path"): wp = renewalparams["webroot_path"] if isinstance(wp, str): # prior to 0.1.0, webroot_path was a string wp = [wp] config.webroot_path = wp def _restore_plugin_configs(config: configuration.NamespaceConfig, renewalparams: Mapping[str, Any]) -> None: """Sets plugin specific values in config from renewalparams :param configuration.NamespaceConfig config: configuration for the current lineage :param configobj.Section renewalparams: Parameters from the renewal configuration file that defines this lineage """ # Now use parser to get plugin-prefixed items with correct types # XXX: the current approach of extracting only prefixed items # related to the actually-used installer and authenticator # works as long as plugins don't need to read plugin-specific # variables set by someone else (e.g., assuming Apache # configurator doesn't need to read webroot_ variables). # Note: if a parameter that used to be defined in the parser is no # longer defined, stored copies of that parameter will be # deserialized as strings by this logic even if they were # originally meant to be some other type. plugin_prefixes: List[str] = [] if renewalparams["authenticator"] == "webroot": _restore_webroot_config(config, renewalparams) else: plugin_prefixes.append(renewalparams["authenticator"]) if renewalparams.get("installer") is not None: plugin_prefixes.append(renewalparams["installer"]) for plugin_prefix in set(plugin_prefixes): plugin_prefix = plugin_prefix.replace('-', '_') for config_item, config_value in renewalparams.items(): if config_item.startswith(plugin_prefix + "_") and not config.set_by_user(config_item): # Values None, True, and False need to be treated specially, # As their types aren't handled correctly by configobj if config_value in ("None", "True", "False"): # bool("False") == True # pylint: disable=eval-used setattr(config, config_item, eval(config_value)) else: cast = cli.argparse_type(config_item) setattr(config, config_item, cast(config_value)) def restore_required_config_elements(config: configuration.NamespaceConfig, renewalparams: Mapping[str, Any]) -> None: """Sets non-plugin specific values in config from renewalparams :param configuration.NamespaceConfig config: configuration for the current lineage :param configobj.Section renewalparams: parameters from the renewal configuration file that defines this lineage """ updated_values = {} required_items = itertools.chain( (("pref_challs", _restore_pref_challs),), zip(BOOL_CONFIG_ITEMS, itertools.repeat(_restore_bool)), zip(INT_CONFIG_ITEMS, itertools.repeat(_restore_int)), zip(STR_CONFIG_ITEMS, itertools.repeat(_restore_str))) for item_name, restore_func in required_items: if item_name in renewalparams and not config.set_by_user(item_name): value = restore_func(item_name, renewalparams[item_name]) updated_values[item_name] = value for key, value in updated_values.items(): setattr(config, key, value) def _remove_deprecated_config_elements(renewalparams: Mapping[str, Any]) -> Dict[str, Any]: """Removes deprecated config options from the parsed renewalparams. :param dict renewalparams: list of parsed renewalparams :returns: list of renewalparams with deprecated config options removed :rtype: dict """ return {option_name: v for (option_name, v) in renewalparams.items() if option_name not in cli.DEPRECATED_OPTIONS} def _restore_pref_challs(unused_name: str, value: Union[List[str], str]) -> List[str]: """Restores preferred challenges from a renewal config file. If value is a `str`, it should be a single challenge type. :param str unused_name: option name :param value: option value :type value: `list` of `str` or `str` :returns: converted option value to be stored in the runtime config :rtype: `list` of `str` :raises errors.Error: if value can't be converted to a bool """ # If pref_challs has only one element, configobj saves the value # with a trailing comma so it's parsed as a list. If this comma is # removed by the user, the value is parsed as a str. value = [value] if isinstance(value, str) else value return cli.parse_preferred_challenges(value) def _restore_bool(name: str, value: str) -> bool: """Restores a boolean 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: bool :raises errors.Error: if value can't be converted to a bool """ lowercase_value = value.lower() if lowercase_value not in ("true", "false"): raise errors.Error(f"Expected True or False for {name} but found {value}") return lowercase_value == "true" def _restore_int(name: str, value: str) -> int: """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(f"Expected a numeric value for {name}") def _restore_str(name: str, value: str) -> Optional[str]: """Restores a string 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: str or None """ # Previous to v0.5.0, Certbot always stored the `server` URL in the renewal config, # resulting in configs which explicitly use the deprecated ACMEv1 URL, today # preventing an automatic transition to the default modern ACME URL. # (https://github.com/certbot/certbot/issues/7978#issuecomment-625442870) # As a mitigation, this function reinterprets the value of the `server` parameter if # necessary, replacing the ACMEv1 URL with the default ACME URL. It is still possible # to override this choice with the explicit `--server` CLI flag. if name == "server" and value == constants.V1_URI: logger.info("Using server %s instead of legacy %s", constants.CLI_DEFAULTS["server"], value) return constants.CLI_DEFAULTS["server"] return None if value == "None" else value def should_renew(config: configuration.NamespaceConfig, lineage: storage.RenewableCert) -> bool: """Return true if any of the circumstances for automatic renewal apply.""" if config.renew_by_default: logger.debug("Auto-renewal forced with --force-renewal...") return True if lineage.should_autorenew(): logger.info("Certificate is due for renewal, auto-renewing...") return True if config.dry_run: logger.info("Certificate not due for renewal, but simulating renewal for dry run") return True display_util.notify("Certificate not yet due for renewal") return False def _avoid_invalidating_lineage(config: configuration.NamespaceConfig, lineage: storage.RenewableCert, original_server: str) -> None: """Do not renew a valid cert with one from a staging server!""" if util.is_staging(config.server): if not util.is_staging(original_server): if not config.break_my_certs: names = ", ".join(lineage.names()) raise errors.Error( "You've asked to renew/replace a seemingly valid certificate with " f"a test certificate (domains: {names}). We will not do that " "unless you use the --break-my-certs flag!") def _avoid_reuse_key_conflicts(config: configuration.NamespaceConfig, lineage: storage.RenewableCert) -> None: """Don't allow combining --reuse-key with any flags that would conflict with key reuse (--key-type, --rsa-key-size, --elliptic-curve), unless --new-key is also set. """ # If --no-reuse-key is set, no conflict if config.set_by_user("reuse_key") and not config.reuse_key: return # If reuse_key is not set on the lineage and --reuse-key is not # set on the CLI, no conflict. if not lineage.reuse_key and not config.reuse_key: return # If --new-key is set, no conflict if config.new_key: return kt = config.key_type.lower() # The remaining cases where conflicts are present: # - --key-type is set on the CLI and doesn't match the stored private key # - It's an RSA key and --rsa-key-size is set and doesn't match # - It's an ECDSA key and --eliptic-curve is set and doesn't match potential_conflicts = [ ("--key-type", lambda: kt != lineage.private_key_type.lower()), ("--rsa-key-size", lambda: kt == "rsa" and config.rsa_key_size != lineage.rsa_key_size), ("--elliptic-curve", lambda: kt == "ecdsa" and lineage.elliptic_curve and \ config.elliptic_curve.lower() != lineage.elliptic_curve.lower()) ] for conflict in potential_conflicts: if conflict[1](): raise errors.Error( f"Unable to change the {conflict[0]} of this certificate because --reuse-key " "is set. To stop reusing the private key, specify --no-reuse-key. " "To change the private key this one time and then reuse it in future, " "add --new-key.") def renew_cert(config: configuration.NamespaceConfig, domains: Optional[List[str]], le_client: client.Client, lineage: storage.RenewableCert) -> None: """Renew a certificate lineage.""" renewal_params = lineage.configuration["renewalparams"] original_server = renewal_params.get("server", cli.flag_default("server")) _avoid_invalidating_lineage(config, lineage, original_server) _avoid_reuse_key_conflicts(config, lineage) if not domains: domains = lineage.names() # The private key is the existing lineage private key if reuse_key is set. # Otherwise, generate a fresh private key by passing None. if config.reuse_key and not config.new_key: new_key = os.path.normpath(lineage.privkey) _update_renewal_params_from_key(new_key, config) else: new_key = None new_cert, new_chain, new_key, _ = le_client.obtain_certificate(domains, new_key) if config.dry_run: logger.debug("Dry run: skipping updating lineage at %s", os.path.dirname(lineage.cert)) else: prior_version = lineage.latest_common_version() # TODO: Check return value of save_successor lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, config) lineage.update_all_links_to(lineage.latest_common_version()) lineage.truncate() hooks.renew_hook(config, domains, lineage.live_dir) def report(msgs: Iterable[str], category: str) -> str: """Format a results report for a category of renewal outcomes""" lines = ("%s (%s)" % (m, category) for m in msgs) return " " + "\n ".join(lines) def _renew_describe_results(config: configuration.NamespaceConfig, renew_successes: List[str], renew_failures: List[str], renew_skipped: List[str], parse_failures: List[str]) -> None: """ Print a report to the terminal about the results of the renewal process. :param configuration.NamespaceConfiguration config: Configuration :param list renew_successes: list of fullchain paths which were renewed :param list renew_failures: list of fullchain paths which failed to be renewed :param list renew_skipped: list of messages to print about skipped certificates :param list parse_failures: list of renewal parameter paths which had errors """ notify = display_util.notify notify_error = logger.error notify(f'\n{display_obj.SIDE_FRAME}') renewal_noun = "simulated renewal" if config.dry_run else "renewal" if renew_skipped: notify("The following certificates are not due for renewal yet:") notify(report(renew_skipped, "skipped")) if not renew_successes and not renew_failures: notify(f"No {renewal_noun}s were attempted.") if (config.pre_hook is not None or config.renew_hook is not None or config.post_hook is not None): notify("No hooks were run.") elif renew_successes and not renew_failures: notify(f"Congratulations, all {renewal_noun}s succeeded: ") notify(report(renew_successes, "success")) elif renew_failures and not renew_successes: notify_error("All %ss failed. The following certificates could " "not be renewed:", renewal_noun) notify_error(report(renew_failures, "failure")) elif renew_failures and renew_successes: notify(f"The following {renewal_noun}s succeeded:") notify(report(renew_successes, "success") + "\n") notify_error("The following %ss failed:", renewal_noun) notify_error(report(renew_failures, "failure")) if parse_failures: notify("\nAdditionally, the following renewal configurations " "were invalid: ") notify(report(parse_failures, "parsefail")) notify(display_obj.SIDE_FRAME) def handle_renewal_request(config: configuration.NamespaceConfig) -> Tuple[list, list]: """Examine each lineage; renew if due and report results""" # This is trivially False if config.domains is empty if any(domain not in config.webroot_map for domain in config.domains): # If more plugins start using cli.add_domains, # we may want to only log a warning here raise errors.Error("Currently, the renew verb is capable of either " "renewing all installed certificates that are due " "to be renewed or renewing a single certificate specified " "by its name. If you would like to renew specific " "certificates by their domains, use the certonly command " "instead. The renew verb may provide other options " "for selecting certificates to renew in the future.") if config.certname: conf_files = [storage.renewal_file_for_certname(config, config.certname)] else: conf_files = storage.renewal_conf_files(config) renew_successes = [] renew_failures = [] renew_skipped = [] parse_failures = [] renewed_domains = [] failed_domains = [] # Noninteractive renewals include a random delay in order to spread # out the load on the certificate authority servers, even if many # users all pick the same time for renewals. This delay precedes # running any hooks, so that side effects of the hooks (such as # shutting down a web service) aren't prolonged unnecessarily. apply_random_sleep = not sys.stdin.isatty() and config.random_sleep_on_renew for renewal_file in conf_files: display_util.notification("Processing " + renewal_file, pause=False) lineage_config = copy.deepcopy(config) lineagename = storage.lineagename_for_filename(renewal_file) # Note that this modifies config (to add back the configuration # elements from within the renewal configuration file). try: renewal_candidate = reconstitute(lineage_config, renewal_file) except Exception as e: # pylint: disable=broad-except logger.error("Renewal configuration file %s (cert: %s) " "produced an unexpected error: %s. Skipping.", renewal_file, lineagename, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) parse_failures.append(renewal_file) continue try: if not renewal_candidate: parse_failures.append(renewal_file) else: renewal_candidate.ensure_deployed() from certbot._internal import main plugins = plugins_disco.PluginsRegistry.find_all() if should_renew(lineage_config, renewal_candidate): # Apply random sleep upon first renewal if needed if apply_random_sleep: sleep_time = random.uniform(1, 60 * 8) logger.info("Non-interactive renewal: random delay of %s seconds", sleep_time) time.sleep(sleep_time) # We will sleep only once this day, folks. apply_random_sleep = False # domains have been restored into lineage_config by reconstitute # but they're unnecessary anyway because renew_cert here # will just grab them from the certificate # we already know it's time to renew based on should_renew # and we have a lineage in renewal_candidate main.renew_cert(lineage_config, plugins, renewal_candidate) renew_successes.append(renewal_candidate.fullchain) renewed_domains.extend(renewal_candidate.names()) else: expiry = crypto_util.notAfter(renewal_candidate.version( "cert", renewal_candidate.latest_common_version())) renew_skipped.append("%s expires on %s" % (renewal_candidate.fullchain, expiry.strftime("%Y-%m-%d"))) # Run updater interface methods updater.run_generic_updaters(lineage_config, renewal_candidate, plugins) except Exception as e: # pylint: disable=broad-except # obtain_cert (presumably) encountered an unanticipated problem. logger.error( "Failed to renew certificate %s with error: %s", lineagename, e ) logger.debug("Traceback was:\n%s", traceback.format_exc()) if renewal_candidate: renew_failures.append(renewal_candidate.fullchain) failed_domains.extend(renewal_candidate.names()) # Describe all the results _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures) if renew_failures or parse_failures: raise errors.Error( f"{len(renew_failures)} renew failure(s), {len(parse_failures)} parse failure(s)") # Windows installer integration tests rely on handle_renewal_request behavior here. # If the text below changes, these tests will need to be updated accordingly. logger.debug("no renewal failures") return (renewed_domains, failed_domains) def _update_renewal_params_from_key(key_path: str, config: configuration.NamespaceConfig) -> None: with open(key_path, 'rb') as file_h: key = load_pem_private_key(file_h.read(), password=None, backend=default_backend()) if isinstance(key, rsa.RSAPrivateKey): config.key_type = 'rsa' config.rsa_key_size = key.key_size elif isinstance(key, ec.EllipticCurvePrivateKey): config.key_type = 'ecdsa' config.elliptic_curve = key.curve.name else: raise errors.Error(f'Key at {key_path} is of an unsupported type: {type(key)}.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/snap_config.py0000664000175100017510000000717214561227515020454 0ustar00ericaerica"""Module configuring Certbot in a snap environment""" import logging import socket from typing import Iterable from typing import List from typing import Optional from requests import Session from requests.adapters import HTTPAdapter from requests.exceptions import HTTPError from requests.exceptions import RequestException from certbot.compat import os from certbot.errors import Error try: from urllib3.connection import HTTPConnection from urllib3.connectionpool import HTTPConnectionPool except ImportError: # Stub imports for oldest requirements, that will never be used in snaps. HTTPConnection = object # type: ignore[misc,assignment] HTTPConnectionPool = object # type: ignore[misc,assignment] _ARCH_TRIPLET_MAP = { 'arm64': 'aarch64-linux-gnu', 'armhf': 'arm-linux-gnueabihf', 'i386': 'i386-linux-gnu', 'ppc64el': 'powerpc64le-linux-gnu', 'powerpc': 'powerpc-linux-gnu', 'amd64': 'x86_64-linux-gnu', 's390x': 's390x-linux-gnu', } LOGGER = logging.getLogger(__name__) def prepare_env(cli_args: List[str]) -> List[str]: """ Prepare runtime environment for a certbot execution in snap. :param list cli_args: List of command line arguments :return: Update list of command line arguments :rtype: list """ snap_arch = os.environ.get('SNAP_ARCH') if snap_arch not in _ARCH_TRIPLET_MAP: raise Error('Unrecognized value of SNAP_ARCH: {0}'.format(snap_arch)) os.environ['CERTBOT_AUGEAS_PATH'] = '{0}/usr/lib/{1}/libaugeas.so.0'.format( os.environ.get('SNAP'), _ARCH_TRIPLET_MAP[snap_arch]) with Session() as session: session.mount('http://snapd/', _SnapdAdapter()) try: response = session.get('http://snapd/v2/connections?snap=certbot&interface=content', timeout=30.0) response.raise_for_status() except RequestException as e: if isinstance(e, HTTPError) and e.response.status_code == 404: LOGGER.error('An error occurred while fetching Certbot snap plugins: ' 'your version of snapd is outdated.') LOGGER.error('Please run "sudo snap install core; sudo snap refresh core" ' 'in your terminal and try again.') else: LOGGER.error('An error occurred while fetching Certbot snap plugins: ' 'make sure the snapd service is running.') raise e data = response.json() connections = ['/snap/{0}/current/lib/python3.8/site-packages/'.format(item['slot']['snap']) for item in data.get('result', {}).get('established', []) if item.get('plug', {}).get('plug') == 'plugin' and item.get('plug-attrs', {}).get('content') == 'certbot-1'] os.environ['CERTBOT_PLUGIN_PATH'] = ':'.join(connections) cli_args.append('--preconfigured-renewal') return cli_args class _SnapdConnection(HTTPConnection): def __init__(self) -> None: super().__init__("localhost") self.sock: Optional[socket.socket] = None def connect(self) -> None: self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect("/run/snapd.socket") class _SnapdConnectionPool(HTTPConnectionPool): def __init__(self) -> None: super().__init__("localhost") def _new_conn(self) -> _SnapdConnection: return _SnapdConnection() class _SnapdAdapter(HTTPAdapter): def get_connection(self, url: str, proxies: Optional[Iterable[str]] = None) -> _SnapdConnectionPool: return _SnapdConnectionPool() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/storage.py0000664000175100017510000015030114561227515017623 0ustar00ericaerica"""Renewable certificates storage.""" # pylint: disable=too-many-lines import datetime import glob import logging import re import shutil import stat from typing import Any from typing import cast from typing import Dict from typing import Iterable from typing import List from typing import Mapping from typing import Optional from typing import Tuple from typing import Union import configobj from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey from cryptography.hazmat.primitives.serialization import load_pem_private_key import parsedatetime import pytz import certbot from certbot import configuration from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot import ocsp from certbot import util from certbot._internal import constants from certbot._internal import error_handler from certbot._internal.plugins import disco as plugins_disco from certbot.compat import filesystem from certbot.compat import os from certbot.plugins import common as plugins_common from certbot.util import parse_loose_version logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") README = "README" CURRENT_VERSION = parse_loose_version(certbot.__version__) BASE_PRIVKEY_MODE = 0o600 # pylint: disable=too-many-lines def renewal_conf_files(config: configuration.NamespaceConfig) -> List[str]: """Build a list of all renewal configuration files. :param configuration.NamespaceConfig config: Configuration object :returns: list of renewal configuration files :rtype: `list` of `str` """ result = glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) result.sort() return result def renewal_file_for_certname(config: configuration.NamespaceConfig, certname: str) -> str: """Return /path/to/certname.conf in the renewal conf directory""" path = os.path.join(config.renewal_configs_dir, f"{certname}.conf") if not os.path.exists(path): raise errors.CertStorageError( f"No certificate found with name {certname} (expected {path}).") return path def cert_path_for_cert_name(config: configuration.NamespaceConfig, cert_name: str) -> str: """ If `--cert-name` was specified, but you need a value for `--cert-path`. :param configuration.NamespaceConfig config: parsed command line arguments :param str cert_name: cert name. """ cert_name_implied_conf = renewal_file_for_certname(config, cert_name) return configobj.ConfigObj( cert_name_implied_conf, encoding='utf-8', default_encoding='utf-8')["fullchain"] def config_with_defaults(config: Optional[configuration.NamespaceConfig] = None ) -> configobj.ConfigObj: """Merge supplied config, if provided, on top of builtin defaults.""" defaults_copy = configobj.ConfigObj( constants.RENEWER_DEFAULTS, encoding='utf-8', default_encoding='utf-8') defaults_copy.merge(config if config is not None else configobj.ConfigObj( encoding='utf-8', default_encoding='utf-8')) return defaults_copy def add_time_interval(base_time: datetime.datetime, interval: str, textparser: parsedatetime.Calendar = parsedatetime.Calendar() ) -> datetime.datetime: """Parse the time specified time interval, and add it to the base_time The interval can be in the English-language format understood by parsedatetime, e.g., '10 days', '3 weeks', '6 months', '9 hours', or a sequence of such intervals like '6 months 1 week' or '3 days 12 hours'. If an integer is found with no associated unit, it is interpreted by default as a number of days. :param datetime.datetime base_time: The time to be added with the interval. :param str interval: The time interval to parse. :returns: The base_time plus the interpretation of the time interval. :rtype: :class:`datetime.datetime`""" if interval.strip().isdigit(): interval += " days" # try to use the same timezone, but fallback to UTC tzinfo = base_time.tzinfo or pytz.UTC return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0] def write_renewal_config(o_filename: str, n_filename: str, archive_dir: str, target: Mapping[str, str], relevant_data: Mapping[str, Any]) -> configobj.ConfigObj: """Writes a renewal config file with the specified name and values. :param str o_filename: Absolute path to the previous version of config file :param str n_filename: Absolute path to the new destination of config file :param str archive_dir: Absolute path to the archive directory :param dict target: Maps ALL_FOUR to their symlink paths :param dict relevant_data: Renewal configuration options to save :returns: Configuration object for the new config file :rtype: configobj.ConfigObj """ config = configobj.ConfigObj(o_filename, encoding='utf-8', default_encoding='utf-8') config["version"] = certbot.__version__ config["archive_dir"] = archive_dir for kind in ALL_FOUR: config[kind] = target[kind] if "renewalparams" not in config: config["renewalparams"] = {} config.comments["renewalparams"] = ["", "Options used in " "the renewal process"] config["renewalparams"].update(relevant_data) for k in config["renewalparams"]: if k not in relevant_data: del config["renewalparams"][k] if "renew_before_expiry" not in config: default_interval = constants.RENEWER_DEFAULTS["renew_before_expiry"] config.initial_comment = ["renew_before_expiry = " + default_interval] # TODO: add human-readable comments explaining other available # parameters logger.debug("Writing new config %s.", n_filename) # Ensure that the file exists with open(n_filename, 'a'): pass # Copy permissions from the old version of the file, if it exists. if os.path.exists(o_filename): current_permissions = stat.S_IMODE(os.lstat(o_filename).st_mode) filesystem.chmod(n_filename, current_permissions) with open(n_filename, "wb") as f: config.write(outfile=f) return config def rename_renewal_config(prev_name: str, new_name: str, cli_config: configuration.NamespaceConfig) -> None: """Renames cli_config.certname's config to cli_config.new_certname. :param .NamespaceConfig cli_config: parsed command line arguments """ prev_filename = renewal_filename_for_lineagename(cli_config, prev_name) new_filename = renewal_filename_for_lineagename(cli_config, new_name) if os.path.exists(new_filename): raise errors.ConfigurationError("The new certificate name " "is already in use.") try: filesystem.replace(prev_filename, new_filename) except OSError: raise errors.ConfigurationError("Please specify a valid filename " "for the new certificate name.") def update_configuration(lineagename: str, archive_dir: str, target: Mapping[str, str], cli_config: configuration.NamespaceConfig) -> configobj.ConfigObj: """Modifies lineagename's config to contain the specified values. :param str lineagename: Name of the lineage being modified :param str archive_dir: Absolute path to the archive directory :param dict target: Maps ALL_FOUR to their symlink paths :param .NamespaceConfig cli_config: parsed command line arguments :returns: Configuration object for the updated config file :rtype: configobj.ConfigObj """ config_filename = renewal_filename_for_lineagename(cli_config, lineagename) temp_filename = config_filename + ".new" # If an existing tempfile exists, delete it if os.path.exists(temp_filename): os.unlink(temp_filename) # Save only the config items that are relevant to renewal values = relevant_values(cli_config) write_renewal_config(config_filename, temp_filename, archive_dir, target, values) filesystem.replace(temp_filename, config_filename) return configobj.ConfigObj(config_filename, encoding='utf-8', default_encoding='utf-8') def get_link_target(link: str) -> str: """Get an absolute path to the target of link. :param str link: Path to a symbolic link :returns: Absolute path to the target of link :rtype: str :raises .CertStorageError: If link does not exists. """ try: target = filesystem.readlink(link) except OSError: raise errors.CertStorageError( "Expected {0} to be a symlink".format(link)) if not os.path.isabs(target): target = os.path.join(os.path.dirname(link), target) return os.path.abspath(target) def _write_live_readme_to(readme_path: str, is_base_dir: bool = False) -> None: prefix = "" if is_base_dir: prefix = "[cert name]/" with open(readme_path, "w") as f: logger.debug("Writing README to %s.", readme_path) f.write("This directory contains your keys and certificates.\n\n" "`{prefix}privkey.pem` : the private key for your certificate.\n" "`{prefix}fullchain.pem`: the certificate file used in most server software.\n" "`{prefix}chain.pem` : used for OCSP stapling in Nginx >=1.3.7.\n" "`{prefix}cert.pem` : will break many server configurations, and " "should not be used\n" " without reading further documentation (see link below).\n\n" "WARNING: DO NOT MOVE OR RENAME THESE FILES!\n" " Certbot expects these files to remain in this location in order\n" " to function properly!\n\n" "We recommend not moving these files. For more information, see the Certbot\n" "User Guide at https://certbot.eff.org/docs/using.html#where-are-my-" "certificates.\n".format(prefix=prefix)) def _relevant(namespaces: Iterable[str], option: str) -> bool: """ Is this option one that could be restored for future renewal purposes? :param namespaces: plugin namespaces for configuration options :type namespaces: `list` of `str` :param str option: the name of the option :rtype: bool """ from certbot._internal import renewal return (option in renewal.CONFIG_ITEMS or any(option.startswith(namespace) for namespace in namespaces)) def relevant_values(config: configuration.NamespaceConfig) -> Dict[str, Any]: """Return a new dict containing only items relevant for renewal. :param .NamespaceConfig config: parsed command line :returns: A new dictionary containing items that can be used in renewal. :rtype dict: """ all_values = config.to_dict() plugins = plugins_disco.PluginsRegistry.find_all() namespaces = [plugins_common.dest_namespace(plugin) for plugin in plugins] rv = { option: value for option, value in all_values.items() if _relevant(namespaces, option) and config.set_by_user(option) } # We always save the server value to help with forward compatibility # and behavioral consistency when versions of Certbot with different # server defaults are used. rv["server"] = all_values["server"] # Save key type to help with forward compatibility on Certbot's transition # from RSA to ECDSA certificates by default. rv["key_type"] = all_values["key_type"] return rv def lineagename_for_filename(config_filename: str) -> str: """Returns the lineagename for a configuration filename. """ if not config_filename.endswith(".conf"): raise errors.CertStorageError( "renewal config file name must end in .conf") return os.path.basename(config_filename[:-len(".conf")]) def renewal_filename_for_lineagename(config: configuration.NamespaceConfig, lineagename: str) -> str: """Returns the lineagename for a configuration filename. """ return os.path.join(config.renewal_configs_dir, lineagename) + ".conf" def _relpath_from_file(archive_dir: str, from_file: str) -> str: """Path to a directory from a file""" return os.path.relpath(archive_dir, os.path.dirname(from_file)) def full_archive_path(config_obj: configobj.ConfigObj, cli_config: configuration.NamespaceConfig, lineagename: str) -> str: """Returns the full archive path for a lineagename Uses cli_config to determine archive path if not available from config_obj. :param configobj.ConfigObj config_obj: Renewal conf file contents (can be None) :param configuration.NamespaceConfig cli_config: Main config file :param str lineagename: Certificate name """ if config_obj and "archive_dir" in config_obj: return config_obj["archive_dir"] return os.path.join(cli_config.default_archive_dir, lineagename) def _full_live_path(cli_config: configuration.NamespaceConfig, lineagename: str) -> str: """Returns the full default live path for a lineagename""" return os.path.join(cli_config.live_dir, lineagename) def delete_files(config: configuration.NamespaceConfig, certname: str) -> None: """Delete all files related to the certificate. If some files are not found, ignore them and continue. """ renewal_filename = renewal_file_for_certname(config, certname) # file exists full_default_archive_dir = full_archive_path(None, config, certname) full_default_live_dir = _full_live_path(config, certname) try: renewal_config = configobj.ConfigObj( renewal_filename, encoding='utf-8', default_encoding='utf-8') except configobj.ConfigObjError: # config is corrupted logger.error("Could not parse %s. You may wish to manually " "delete the contents of %s and %s.", renewal_filename, full_default_live_dir, full_default_archive_dir) raise errors.CertStorageError( "error parsing {0}".format(renewal_filename)) finally: # we couldn't read it, but let's at least delete it # if this was going to fail, it already would have. os.remove(renewal_filename) logger.info("Removed %s", renewal_filename) # cert files and (hopefully) live directory # it's not guaranteed that the files are in our default storage # structure. so, first delete the cert files. directory_names = set() for kind in ALL_FOUR: link = renewal_config.get(kind) try: os.remove(link) logger.debug("Removed %s", link) except OSError: logger.debug("Unable to delete %s", link) directory = os.path.dirname(link) directory_names.add(directory) # if all four were in the same directory, and the only thing left # is the README file (or nothing), delete that directory. # this will be wrong in very few but some cases. if len(directory_names) == 1: # delete the README file directory = directory_names.pop() readme_path = os.path.join(directory, README) try: os.remove(readme_path) logger.debug("Removed %s", readme_path) except OSError: logger.debug("Unable to delete %s", readme_path) # if it's now empty, delete the directory try: os.rmdir(directory) # only removes empty directories logger.debug("Removed %s", directory) except OSError: logger.debug("Unable to remove %s; may not be empty.", directory) # archive directory try: archive_path = full_archive_path(renewal_config, config, certname) shutil.rmtree(archive_path) logger.debug("Removed %s", archive_path) except OSError: logger.debug("Unable to remove %s", archive_path) class RenewableCert(interfaces.RenewableCert): """Renewable certificate. Represents a lineage of certificates that is under the management of Certbot, indicated by the existence of an associated renewal configuration file. Note that the notion of "current version" for a lineage is maintained on disk in the structure of symbolic links, and is not explicitly stored in any instance variable in this object. The RenewableCert object is able to determine information about the current (or other) version by accessing data on disk, but does not inherently know any of this information except by examining the symbolic links as needed. The instance variables mentioned below point to symlinks that reflect the notion of "current version" of each managed object, and it is these paths that should be used when configuring servers to use the certificate managed in a lineage. These paths are normally within the "live" directory, and their symlink targets -- the actual cert files -- are normally found within the "archive" directory. :ivar str cert: The path to the symlink representing the current version of the certificate managed by this lineage. :ivar str privkey: The path to the symlink representing the current version of the private key managed by this lineage. :ivar str chain: The path to the symlink representing the current version of the chain managed by this lineage. :ivar str fullchain: The path to the symlink representing the current version of the fullchain (combined chain and cert) managed by this lineage. :ivar configobj.ConfigObj configuration: The renewal configuration options associated with this lineage, obtained from parsing the renewal configuration file and/or systemwide defaults. """ def __init__(self, config_filename: str, cli_config: configuration.NamespaceConfig, update_symlinks: bool = False) -> None: """Instantiate a RenewableCert object from an existing lineage. :param str config_filename: the path to the renewal config file that defines this lineage. :param .NamespaceConfig: parsed command line arguments :raises .CertStorageError: if the configuration file's name didn't end in ".conf", or the file is missing or broken. """ self.cli_config = cli_config self._lineagename = lineagename_for_filename(config_filename) # self.configuration should be used to read parameters that # may have been chosen based on default values from the # systemwide renewal configuration; self.configfile should be # used to make and save changes. try: self.configfile = configobj.ConfigObj( config_filename, encoding='utf-8', default_encoding='utf-8') except configobj.ConfigObjError: raise errors.CertStorageError( "error parsing {0}".format(config_filename)) # TODO: Do we actually use anything from defaults and do we want to # read further defaults from the systemwide renewal configuration # file at this stage? self.configuration = config_with_defaults(self.configfile) if not all(x in self.configuration for x in ALL_FOUR): raise errors.CertStorageError( "renewal config file {0} is missing a required " "file reference".format(self.configfile)) conf_version = self.configuration.get("version") if (conf_version is not None and parse_loose_version(conf_version) > CURRENT_VERSION): logger.info( "Attempting to parse the version %s renewal configuration " "file found at %s with version %s of Certbot. This might not " "work.", conf_version, config_filename, certbot.__version__) self.cert = self.configuration["cert"] self.privkey = self.configuration["privkey"] self.chain = self.configuration["chain"] self.fullchain = self.configuration["fullchain"] self.live_dir = os.path.dirname(self.cert) self._fix_symlinks() if update_symlinks: self._update_symlinks() self._check_symlinks() @property def key_path(self) -> str: """Duck type for self.privkey""" return self.privkey @property def cert_path(self) -> str: """Duck type for self.cert""" return self.cert @property def chain_path(self) -> str: """Duck type for self.chain""" return self.chain @property def fullchain_path(self) -> str: """Duck type for self.fullchain""" return self.fullchain @property def lineagename(self) -> str: """Name given to the certificate lineage. :rtype: str """ return self._lineagename @property def target_expiry(self) -> datetime.datetime: """The current target certificate's expiration datetime :returns: Expiration datetime of the current target certificate :rtype: :class:`datetime.datetime` """ cert_path = self.current_target("cert") if not cert_path: raise errors.Error("Target certificate does not exist.") return crypto_util.notAfter(cert_path) @property def archive_dir(self) -> str: """Returns the default or specified archive directory""" return full_archive_path(self.configuration, self.cli_config, self.lineagename) def relative_archive_dir(self, from_file: str) -> str: """Returns the default or specified archive directory as a relative path Used for creating symbolic links. """ return _relpath_from_file(self.archive_dir, from_file) @property def server(self) -> Optional[str]: """Returns the ACME server associated with this certificate""" return self.configuration["renewalparams"].get("server", None) @property def is_test_cert(self) -> bool: """Returns true if this is a test cert from a staging server.""" if self.server: return util.is_staging(self.server) return False @property def reuse_key(self) -> bool: """Returns whether this certificate is configured to reuse its private key""" return "reuse_key" in self.configuration["renewalparams"] and \ self.configuration["renewalparams"].as_bool("reuse_key") def _check_symlinks(self) -> None: """Raises an exception if a symlink doesn't exist""" for kind in ALL_FOUR: link = getattr(self, kind) if not os.path.islink(link): raise errors.CertStorageError( "expected {0} to be a symlink".format(link)) target = get_link_target(link) if not os.path.exists(target): raise errors.CertStorageError("target {0} of symlink {1} does " "not exist".format(target, link)) def _update_symlinks(self) -> None: """Updates symlinks to use archive_dir""" for kind in ALL_FOUR: link = getattr(self, kind) previous_link = get_link_target(link) new_link = os.path.join(self.relative_archive_dir(link), os.path.basename(previous_link)) os.unlink(link) os.symlink(new_link, link) def _consistent(self) -> bool: """Are the files associated with this lineage self-consistent? :returns: Whether the files stored in connection with this lineage appear to be correct and consistent with one another. :rtype: bool """ # Each element must be referenced with an absolute path for x in (self.cert, self.privkey, self.chain, self.fullchain): if not os.path.isabs(x): logger.debug("Element %s is not referenced with an " "absolute path.", x) return False # Each element must exist and be a symbolic link for x in (self.cert, self.privkey, self.chain, self.fullchain): if not os.path.islink(x): logger.debug("Element %s is not a symbolic link.", x) return False for kind in ALL_FOUR: link = getattr(self, kind) target = get_link_target(link) # Each element's link must point within the cert lineage's # directory within the official archive directory if not os.path.samefile(os.path.dirname(target), self.archive_dir): logger.debug("Element's link does not point within the " "cert lineage's directory within the " "official archive directory. Link: %s, " "target directory: %s, " "archive directory: %s. If you've specified " "the archive directory in the renewal configuration " "file, you may need to update links by running " "certbot update_symlinks.", link, os.path.dirname(target), self.archive_dir) return False # The link must point to a file that exists if not os.path.exists(target): logger.debug("Link %s points to file %s that does not exist.", link, target) return False # The link must point to a file that follows the archive # naming convention pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) if not pattern.match(os.path.basename(target)): logger.debug("%s does not follow the archive naming " "convention.", target) return False # It is NOT required that the link's target be a regular # file (it may itself be a symlink). But we should probably # do a recursive check that ultimately the target does # exist? # XXX: Additional possible consistency checks (e.g. # cryptographic validation of the chain being a chain, # the chain matching the cert, and the cert matching # the subject key) # XXX: All four of the targets are in the same directory # (This check is redundant with the check that they # are all in the desired directory!) # len(set(os.path.basename(self.current_target(x) # for x in ALL_FOUR))) == 1 return True def _fix(self) -> None: """Attempt to fix defects or inconsistencies in this lineage. .. todo:: Currently unimplemented. """ # TODO: Figure out what kinds of fixes are possible. For # example, checking if there is a valid version that # we can update the symlinks to. (Maybe involve # parsing keys and certs to see if they exist and # if a key corresponds to the subject key of a cert?) # TODO: In general, the symlink-reading functions below are not # cautious enough about the possibility that links or their # targets may not exist. (This shouldn't happen, but might # happen as a result of random tampering by a sysadmin, or # filesystem errors, or crashes.) def _previous_symlinks(self) -> List[Tuple[str, str]]: """Returns the kind and path of all symlinks used in recovery. :returns: list of (kind, symlink) tuples :rtype: list """ previous_symlinks = [] for kind in ALL_FOUR: link_dir = os.path.dirname(getattr(self, kind)) link_base = "previous_{0}.pem".format(kind) previous_symlinks.append((kind, os.path.join(link_dir, link_base))) return previous_symlinks def _fix_symlinks(self) -> None: """Fixes symlinks in the event of an incomplete version update. If there is no problem with the current symlinks, this function has no effect. """ previous_symlinks = self._previous_symlinks() if all(os.path.exists(link[1]) for link in previous_symlinks): for kind, previous_link in previous_symlinks: current_link = getattr(self, kind) if os.path.lexists(current_link): os.unlink(current_link) os.symlink(filesystem.readlink(previous_link), current_link) for _, link in previous_symlinks: if os.path.exists(link): os.unlink(link) def current_target(self, kind: str) -> Optional[str]: """Returns full path to which the specified item currently points. :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :returns: The path to the current version of the specified member. :rtype: str or None """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") link = getattr(self, kind) if not os.path.exists(link): logger.debug("Expected symlink %s for %s does not exist.", link, kind) return None return get_link_target(link) def current_version(self, kind: str) -> Optional[int]: """Returns numerical version of the specified item. For example, if kind is "chain" and the current chain link points to a file named "chain7.pem", returns the integer 7. :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :returns: the current version of the specified member. :rtype: int """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) target = self.current_target(kind) if target is None or not os.path.exists(target): logger.debug("Current-version target for %s " "does not exist at %s.", kind, target) target = "" matches = pattern.match(os.path.basename(target)) if matches: return int(matches.groups()[0]) logger.debug("No matches for target %s.", kind) return None def version(self, kind: str, version: int) -> str: """The filename that corresponds to the specified version and kind. .. warning:: The specified version may not exist in this lineage. There is no guarantee that the file path returned by this method actually exists. :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :param int version: the desired version :returns: The path to the specified version of the specified member. :rtype: str """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") link = self.current_target(kind) if not link: raise errors.Error(f"Target {kind} does not exist!") where = os.path.dirname(link) return os.path.join(where, "{0}{1}.pem".format(kind, version)) def available_versions(self, kind: str) -> List[int]: """Which alternative versions of the specified kind of item exist? The archive directory where the current version is stored is consulted to obtain the list of alternatives. :param str kind: the lineage member item ( ``cert``, ``privkey``, ``chain``, or ``fullchain``) :returns: all of the version numbers that currently exist :rtype: `list` of `int` """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") link = self.current_target(kind) if not link: raise errors.Error(f"Target {kind} does not exist!") where = os.path.dirname(link) files = os.listdir(where) pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) matches = [pattern.match(f) for f in files] return sorted([int(m.groups()[0]) for m in matches if m]) def newest_available_version(self, kind: str) -> int: """Newest available version of the specified kind of item? :param str kind: the lineage member item (``cert``, ``privkey``, ``chain``, or ``fullchain``) :returns: the newest available version of this member :rtype: int """ return max(self.available_versions(kind)) def latest_common_version(self) -> int: """Newest version for which all items are available? :returns: the newest available version for which all members (``cert, ``privkey``, ``chain``, and ``fullchain``) exist :rtype: int """ # TODO: this can raise CertStorageError if there is no version overlap # (it should probably return None instead) # TODO: this can raise a spurious AttributeError if the current # link for any kind is missing (it should probably return None) versions = [self.available_versions(x) for x in ALL_FOUR] return max(n for n in versions[0] if all(n in v for v in versions[1:])) def next_free_version(self) -> int: """Smallest version newer than all full or partial versions? :returns: the smallest version number that is larger than any version of any item currently stored in this lineage :rtype: int """ # TODO: consider locking/mutual exclusion between updating processes # This isn't self.latest_common_version() + 1 because we don't want # collide with a version that might exist for one file type but not # for the others. return max(self.newest_available_version(x) for x in ALL_FOUR) + 1 def ensure_deployed(self) -> bool: """Make sure we've deployed the latest version. :returns: False if a change was needed, True otherwise :rtype: bool May need to recover from rare interrupted / crashed states.""" if self.has_pending_deployment(): logger.warning("Found a new certificate /archive/ that was not " "linked to in /live/; fixing...") self.update_all_links_to(self.latest_common_version()) return False return True def has_pending_deployment(self) -> bool: """Is there a later version of all of the managed items? :returns: ``True`` if there is a complete version of this lineage with a larger version number than the current version, and ``False`` otherwise :rtype: bool """ all_versions: List[int] = [] for item in ALL_FOUR: version = self.current_version(item) if version is None: raise errors.Error(f"{item} is required but missing for this certificate.") all_versions.append(version) # TODO: consider whether to assume consistency or treat # inconsistent/consistent versions differently smallest_current = min(all_versions) return smallest_current < self.latest_common_version() def _update_link_to(self, kind: str, version: int) -> None: """Make the specified item point at the specified version. (Note that this method doesn't verify that the specified version exists.) :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :param int version: the desired version """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") link = getattr(self, kind) filename = "{0}{1}.pem".format(kind, version) # Relative rather than absolute target directory target_directory = os.path.dirname(filesystem.readlink(link)) # TODO: it could be safer to make the link first under a temporary # filename, then unlink the old link, then rename the new link # to the old link; this ensures that this process is able to # create symlinks. # TODO: we might also want to check consistency of related links # for the other corresponding items os.unlink(link) os.symlink(os.path.join(target_directory, filename), link) def update_all_links_to(self, version: int) -> None: """Change all member objects to point to the specified version. :param int version: the desired version """ with error_handler.ErrorHandler(self._fix_symlinks): previous_links = self._previous_symlinks() for kind, link in previous_links: target = self.current_target(kind) if not target: raise errors.Error(f"Target {kind} does not exist!") os.symlink(target, link) for kind in ALL_FOUR: self._update_link_to(kind, version) for _, link in previous_links: os.unlink(link) def names(self) -> List[str]: """What are the subject names of this certificate? :returns: the subject names :rtype: `list` of `str` :raises .CertStorageError: if could not find cert file. """ target = self.current_target("cert") if target is None: raise errors.CertStorageError("could not find the certificate file") with open(target, "rb") as f: return crypto_util.get_names_from_cert(f.read()) def ocsp_revoked(self, version: int) -> bool: """Is the specified cert version revoked according to OCSP? Also returns True if the cert version is declared as revoked according to OCSP. If OCSP status could not be determined, False is returned. :param int version: the desired version number :returns: True if the certificate is revoked, otherwise, False :rtype: bool """ cert_path = self.version("cert", version) chain_path = self.version("chain", version) # While the RevocationChecker should return False if it failed to # determine the OCSP status, let's ensure we don't crash Certbot by # catching all exceptions here. try: return ocsp.RevocationChecker().ocsp_revoked_by_paths(cert_path, chain_path) except Exception as e: # pylint: disable=broad-except logger.warning( "An error occurred determining the OCSP status of %s.", cert_path) logger.debug(str(e)) return False def autorenewal_is_enabled(self) -> bool: """Is automatic renewal enabled for this cert? If autorenew is not specified, defaults to True. :returns: True if automatic renewal is enabled :rtype: bool """ return ("autorenew" not in self.configuration["renewalparams"] or self.configuration["renewalparams"].as_bool("autorenew")) def should_autorenew(self) -> bool: """Should we now try to autorenew the most recent cert version? This is a policy question and does not only depend on whether the cert is expired. (This considers whether autorenewal is enabled, whether the cert is revoked, and whether the time interval for autorenewal has been reached.) Note that this examines the numerically most recent cert version, not the currently deployed version. :returns: whether an attempt should now be made to autorenew the most current cert version in this lineage :rtype: bool """ if self.autorenewal_is_enabled(): # Consider whether to attempt to autorenew this cert now # Renewals on the basis of revocation if self.ocsp_revoked(self.latest_common_version()): logger.debug("Should renew, certificate is revoked.") return True # Renews some period before expiry time default_interval = constants.RENEWER_DEFAULTS["renew_before_expiry"] interval = self.configuration.get("renew_before_expiry", default_interval) expiry = crypto_util.notAfter(self.version( "cert", self.latest_common_version())) now = datetime.datetime.now(pytz.UTC) if expiry < add_time_interval(now, interval): logger.debug("Should renew, less than %s before certificate " "expiry %s.", interval, expiry.strftime("%Y-%m-%d %H:%M:%S %Z")) return True return False @classmethod def new_lineage(cls, lineagename: str, cert: bytes, privkey: bytes, chain: bytes, cli_config: configuration.NamespaceConfig) -> "RenewableCert": """Create a new certificate lineage. Attempts to create a certificate lineage -- enrolled for potential future renewal -- with the (suggested) lineage name lineagename, and the associated cert, privkey, and chain (the associated fullchain will be created automatically). Optional configurator and renewalparams record the configuration that was originally used to obtain this cert, so that it can be reused later during automated renewal. Returns a new RenewableCert object referring to the created lineage. (The actual lineage name, as well as all the relevant file paths, will be available within this object.) :param str lineagename: the suggested name for this lineage (normally the current cert's first subject DNS name) :param str cert: the initial certificate version in PEM format :param str privkey: the private key in PEM format :param str chain: the certificate chain in PEM format :param .NamespaceConfig cli_config: parsed command line arguments :returns: the newly-created RenewalCert object :rtype: :class:`storage.renewableCert` """ # Examine the configuration and find the new lineage's name for i in (cli_config.renewal_configs_dir, cli_config.default_archive_dir, cli_config.live_dir): if not os.path.exists(i): filesystem.makedirs(i, 0o700) logger.debug("Creating directory %s.", i) config_file, config_filename = util.unique_lineage_name( cli_config.renewal_configs_dir, lineagename) base_readme_path = os.path.join(cli_config.live_dir, README) if not os.path.exists(base_readme_path): _write_live_readme_to(base_readme_path, is_base_dir=True) # Determine where on disk everything will go # lineagename will now potentially be modified based on which # renewal configuration file could actually be created lineagename = lineagename_for_filename(config_filename) archive = full_archive_path(None, cli_config, lineagename) live_dir = _full_live_path(cli_config, lineagename) if os.path.exists(archive) and (not os.path.isdir(archive) or os.listdir(archive)): config_file.close() raise errors.CertStorageError( "archive directory exists for " + lineagename) if os.path.exists(live_dir) and (not os.path.isdir(live_dir) or os.listdir(live_dir)): config_file.close() raise errors.CertStorageError( "live directory exists for " + lineagename) for i in (archive, live_dir): if not os.path.exists(i): filesystem.makedirs(i) logger.debug("Creating directory %s.", i) # Put the data into the appropriate files on disk target = {kind: os.path.join(live_dir, kind + ".pem") for kind in ALL_FOUR} archive_target = {kind: os.path.join(archive, kind + "1.pem") for kind in ALL_FOUR} for kind in ALL_FOUR: os.symlink(_relpath_from_file(archive_target[kind], target[kind]), target[kind]) with open(target["cert"], "wb") as f_b: logger.debug("Writing certificate to %s.", target["cert"]) f_b.write(cert) with util.safe_open(archive_target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f_a: logger.debug("Writing private key to %s.", target["privkey"]) f_a.write(privkey) # XXX: Let's make sure to get the file permissions right here with open(target["chain"], "wb") as f_b: logger.debug("Writing chain to %s.", target["chain"]) f_b.write(chain) with open(target["fullchain"], "wb") as f_b: # assumes that OpenSSL.crypto.dump_certificate includes # ending newline character logger.debug("Writing full chain to %s.", target["fullchain"]) f_b.write(cert + chain) # Write a README file to the live directory readme_path = os.path.join(live_dir, README) _write_live_readme_to(readme_path) # Document what we've done in a new renewal config file config_file.close() # Save only the config items that are relevant to renewal values = relevant_values(cli_config) new_config = write_renewal_config(config_filename, config_filename, archive, target, values) return cls(new_config.filename, cli_config) def _private_key(self) -> Union[RSAPrivateKey, EllipticCurvePrivateKey]: with open(self.configuration["privkey"], "rb") as priv_key_file: key = load_pem_private_key( data=priv_key_file.read(), password=None, backend=default_backend() ) return cast(Union[RSAPrivateKey, EllipticCurvePrivateKey], key) @property def private_key_type(self) -> str: """ :returns: The type of algorithm for the private, RSA or ECDSA :rtype: str """ key = self._private_key() if isinstance(key, RSAPrivateKey): return "RSA" return "ECDSA" @property def rsa_key_size(self) -> Optional[int]: """ :returns: If the private key is an RSA key, its size. :rtype: int """ key = self._private_key() if isinstance(key, RSAPrivateKey): return key.key_size return None @property def elliptic_curve(self) -> Optional[str]: """ :returns: If the private key is an elliptic key, the name of its curve. :rtype: str """ key = self._private_key() if isinstance(key, EllipticCurvePrivateKey): return key.curve.name return None def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes, new_chain: bytes, cli_config: configuration.NamespaceConfig) -> int: """Save new cert and chain as a successor of a prior version. Returns the new version number that was created. .. note:: this function does NOT update links to deploy this version :param int prior_version: the old version to which this version is regarded as a successor (used to choose a privkey, if the key has not changed, but otherwise this information is not permanently recorded anywhere) :param bytes new_cert: the new certificate, in PEM format :param bytes new_privkey: the new private key, in PEM format, or ``None``, if the private key has not changed :param bytes new_chain: the new chain, in PEM format :param .NamespaceConfig cli_config: parsed command line arguments :returns: the new version number that was created :rtype: int """ # XXX: assumes official archive location rather than examining links # XXX: consider using os.open for availability of os.O_EXCL # XXX: ensure file permissions are correct; also create directories # if needed (ensuring their permissions are correct) # Figure out what the new version is and hence where to save things self.cli_config = cli_config target_version = self.next_free_version() target = {kind: os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, target_version)) for kind in ALL_FOUR} old_privkey = os.path.join( self.archive_dir, "privkey{0}.pem".format(prior_version)) # Distinguish the cases where the privkey has changed and where it # has not changed (in the latter case, making an appropriate symlink # to an earlier privkey version) if new_privkey is None: # The behavior below keeps the prior key by creating a new # symlink to the old key or the target of the old key symlink. if os.path.islink(old_privkey): old_privkey = filesystem.readlink(old_privkey) else: old_privkey = f"privkey{prior_version}.pem" logger.debug("Writing symlink to old private key, %s.", old_privkey) os.symlink(old_privkey, target["privkey"]) else: with util.safe_open(target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f: logger.debug("Writing new private key to %s.", target["privkey"]) f.write(new_privkey) # Preserve gid and (mode & MASK_FOR_PRIVATE_KEY_PERMISSIONS) # from previous privkey in this lineage. mode = filesystem.compute_private_key_mode(old_privkey, BASE_PRIVKEY_MODE) filesystem.copy_ownership_and_apply_mode( old_privkey, target["privkey"], mode, copy_user=False, copy_group=True) # Save everything else with open(target["cert"], "wb") as f: logger.debug("Writing certificate to %s.", target["cert"]) f.write(new_cert) with open(target["chain"], "wb") as f: logger.debug("Writing chain to %s.", target["chain"]) f.write(new_chain) with open(target["fullchain"], "wb") as f: logger.debug("Writing full chain to %s.", target["fullchain"]) f.write(new_cert + new_chain) symlinks = {kind: self.configuration[kind] for kind in ALL_FOUR} # Update renewal config file self.configfile = update_configuration( self.lineagename, self.archive_dir, symlinks, cli_config) self.configuration = config_with_defaults(self.configfile) return target_version def save_new_config_values(self, cli_config: configuration.NamespaceConfig) -> None: """Save only the config information without writing the new cert. :param .NamespaceConfig cli_config: parsed command line arguments """ self.cli_config = cli_config symlinks = {kind: self.configuration[kind] for kind in ALL_FOUR} # Update renewal config file self.configfile = update_configuration( self.lineagename, self.archive_dir, symlinks, cli_config) self.configuration = config_with_defaults(self.configfile) def truncate(self, num_prior_certs_to_keep: int = 5) -> None: """Delete unused historical certificate, chain and key items from the lineage. A certificate version will be deleted if it is: 1. not the current target, and 2. not a previous version within num_prior_certs_to_keep. :param num_prior_certs_to_keep: How many prior certificate versions to keep. """ # Do not want to delete the current or the previous num_prior_certs_to_keep certs current_version = self.latest_common_version() versions_to_delete = set(self.available_versions("cert")) versions_to_delete -= set(range(current_version, current_version - 1 - num_prior_certs_to_keep, -1)) archive = self.archive_dir # Delete the remaining lineage items kinds for those certificate versions. for ver in versions_to_delete: logger.debug("Deleting %s/cert%d.pem and related items during clean up", archive, ver) for kind in ALL_FOUR: item_path = os.path.join(archive, f"{kind}{ver}.pem") try: if os.path.exists(item_path): os.unlink(item_path) except OSError: logger.debug("Failed to clean up %s", item_path, exc_info=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3310835 certbot-2.9.0/certbot/_internal/tests/0000775000175100017510000000000014561227516016750 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/__init__.py0000664000175100017510000000002414561227515021054 0ustar00ericaerica"""certbot tests""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/account_test.py0000664000175100017510000003371414561227515022024 0ustar00ericaerica"""Tests for certbot._internal.account.""" import datetime import json import sys import unittest from unittest import mock import josepy as jose import pytest import pytz from acme import messages from certbot import errors from certbot.compat import filesystem from certbot.compat import misc from certbot.compat import os import certbot.tests.util as test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class AccountTest(unittest.TestCase): """Tests for certbot._internal.account.Account.""" def setUp(self): from certbot._internal.account import Account self.regr = mock.MagicMock() self.meta = Account.Meta( creation_host="test.certbot.org", creation_dt=datetime.datetime( 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC)) self.acc = Account(self.regr, KEY, self.meta) self.regr.__repr__ = mock.MagicMock(return_value="i_am_a_regr") with mock.patch("certbot._internal.account.socket") as mock_socket: mock_socket.getfqdn.return_value = "test.certbot.org" with mock.patch("certbot._internal.account.datetime") as mock_dt: mock_dt.datetime.now.return_value = self.meta.creation_dt self.acc_no_meta = Account(self.regr, KEY) def test_init(self): assert self.regr == self.acc.regr assert KEY == self.acc.key assert self.meta == self.acc_no_meta.meta def test_id(self): assert self.acc.id == "7adac10320f585ddf118429c0c4af2cd" def test_slug(self): assert self.acc.slug == "test.certbot.org@2015-07-04T14:04:10Z (7ada)" def test_repr(self): assert repr(self.acc).startswith( " 3 assert self.mock_auth.cleanup.call_count == 1 # Test if list first element is http-01, use typ because it is an achall assert self.mock_auth.cleanup.call_args[0][0][0].typ == "http-01" assert len(authzr) == 1 def test_name1_http_01_1_acme_2(self): self._test_name1_http_01_1_common() def test_name1_http_01_1_dns_1_acme_2(self): self.mock_net.poll.side_effect = _gen_mock_on_poll() self.mock_auth.get_chall_pref.return_value.append(challenges.DNS01) authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES) mock_order = mock.MagicMock(authorizations=[authzr]) authzr = self.handler.handle_authorizations(mock_order, self.mock_config) assert self.mock_net.answer_challenge.call_count == 1 assert self.mock_net.poll.call_count == 1 assert self.mock_auth.cleanup.call_count == 1 cleaned_up_achalls = self.mock_auth.cleanup.call_args[0][0] assert len(cleaned_up_achalls) == 1 assert cleaned_up_achalls[0].typ == "http-01" # Length of authorizations list assert len(authzr) == 1 def test_name3_http_01_3_common_acme_2(self): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES), gen_dom_authzr(domain="1", challs=acme_util.CHALLENGES), gen_dom_authzr(domain="2", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) self.mock_net.poll.side_effect = _gen_mock_on_poll() authzr = self.handler.handle_authorizations(mock_order, self.mock_config) assert self.mock_net.answer_challenge.call_count == 3 # Check poll call assert self.mock_net.poll.call_count == 3 assert self.mock_auth.cleanup.call_count == 1 assert len(authzr) == 3 def test_debug_challenges(self): config = mock.Mock(debug_challenges=True, verbose_count=0) authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) account_key_thumbprint = b"foobarbaz" self.mock_account.key.thumbprint.return_value = account_key_thumbprint self.mock_net.poll.side_effect = _gen_mock_on_poll() self.handler.handle_authorizations(mock_order, config) assert self.mock_net.answer_challenge.call_count == 1 assert self.mock_display.notification.call_count == 1 assert 'Pass "-v" for more info' in \ self.mock_display.notification.call_args[0][0] assert f"http://{authzrs[0].body.identifier.value}/.well-known/acme-challenge/" + \ b64encode(authzrs[0].body.challenges[0].chall.token).decode() not in \ self.mock_display.notification.call_args[0][0] assert b64encode(account_key_thumbprint).decode() not in \ self.mock_display.notification.call_args[0][0] def test_debug_challenges_verbose(self): config = mock.Mock(debug_challenges=True, verbose_count=1) authzrs = [gen_dom_authzr(domain="0", challs=[acme_util.HTTP01]), gen_dom_authzr(domain="1", challs=[acme_util.DNS01])] mock_order = mock.MagicMock(authorizations=authzrs) account_key_thumbprint = b"foobarbaz" self.mock_account.key.thumbprint.return_value = account_key_thumbprint self.mock_net.poll.side_effect = _gen_mock_on_poll() self.mock_auth.get_chall_pref.return_value = [challenges.HTTP01, challenges.DNS01] self.handler.handle_authorizations(mock_order, config) assert self.mock_net.answer_challenge.call_count == 2 assert self.mock_display.notification.call_count == 1 assert 'Pass "-v" for more info' not in \ self.mock_display.notification.call_args[0][0] assert f"http://{authzrs[0].body.identifier.value}/.well-known/acme-challenge/" + \ b64encode(authzrs[0].body.challenges[0].chall.token).decode() in \ self.mock_display.notification.call_args[0][0] assert b64encode(account_key_thumbprint).decode() in \ self.mock_display.notification.call_args[0][0] assert f"_acme-challenge.{authzrs[1].body.identifier.value}" in \ self.mock_display.notification.call_args[0][0] assert authzrs[1].body.challenges[0].validation(self.mock_account.key) in \ self.mock_display.notification.call_args[0][0] def test_perform_failure(self): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) self.mock_auth.perform.side_effect = errors.AuthorizationError with pytest.raises(errors.AuthorizationError): self.handler.handle_authorizations(mock_order, self.mock_config) def test_max_retries_exceeded(self): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) # We will return STATUS_PENDING twice before returning STATUS_VALID. self.mock_net.poll.side_effect = _gen_mock_on_poll(retry=2) with pytest.raises(errors.AuthorizationError, match='All authorizations were not finalized by the CA.'): # We retry only once, so retries will be exhausted before STATUS_VALID is returned. self.handler.handle_authorizations(mock_order, self.mock_config, False, 1) @mock.patch('certbot._internal.auth_handler.time.sleep') def test_deadline_exceeded(self, mock_sleep): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) orig_now = datetime.datetime.now state = {'time_slept': 0} def mock_sleep_effect(secs): state['time_slept'] += secs mock_sleep.side_effect = mock_sleep_effect def mock_now_effect(): return orig_now() + datetime.timedelta(seconds=state["time_slept"]) # We will return STATUS_PENDING and ask Certbot to sleep for 20 minutes at a time. interval = datetime.timedelta(minutes=20).seconds self.mock_net.poll.side_effect = _gen_mock_on_poll(status=messages.STATUS_PENDING, wait_value=interval) with pytest.raises(errors.AuthorizationError, match='All authorizations were not finalized by the CA.'): with mock.patch('certbot._internal.auth_handler.datetime.datetime') as mock_dt: mock_dt.now.side_effect = mock_now_effect # Polling will only proceed for 30 minutes at most, so the second 20 minute sleep # should be truncated and the polling should be aborted. self.handler.handle_authorizations(mock_order, self.mock_config, False) assert mock_sleep.call_count == 3 # 1s, 20m and 10m sleep assert mock_sleep.call_args_list[0][0][0] == 1 assert abs(mock_sleep.call_args_list[1][0][0] - (interval - 1)) <= 1 assert abs(mock_sleep.call_args_list[2][0][0] - (interval/2 - 1)) <= 1 def test_no_domains(self): mock_order = mock.MagicMock(authorizations=[]) with pytest.raises(errors.AuthorizationError): self.handler.handle_authorizations(mock_order, self.mock_config) def test_preferred_challenge_choice_common_acme_2(self): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) self.handler.pref_challs.extend((challenges.HTTP01.typ, challenges.DNS01.typ,)) self.mock_net.poll.side_effect = _gen_mock_on_poll() self.handler.handle_authorizations(mock_order, self.mock_config) assert self.mock_auth.cleanup.call_count == 1 assert self.mock_auth.cleanup.call_args[0][0][0].typ == "http-01" def test_preferred_challenges_not_supported_acme_2(self): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) self.handler.pref_challs.append(challenges.DNS01.typ) with pytest.raises(errors.AuthorizationError): self.handler.handle_authorizations(mock_order, self.mock_config) def test_dns_only_challenge_not_supported(self): authzrs = [gen_dom_authzr(domain="0", challs=[acme_util.DNS01])] mock_order = mock.MagicMock(authorizations=authzrs) with pytest.raises(errors.AuthorizationError): self.handler.handle_authorizations(mock_order, self.mock_config) def test_perform_error(self): self.mock_auth.perform.side_effect = errors.AuthorizationError authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES) mock_order = mock.MagicMock(authorizations=[authzr]) with pytest.raises(errors.AuthorizationError): self.handler.handle_authorizations(mock_order, self.mock_config) assert self.mock_auth.cleanup.call_count == 1 assert self.mock_auth.cleanup.call_args[0][0][0].typ == "http-01" def test_answer_error(self): self.mock_net.answer_challenge.side_effect = errors.AuthorizationError authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) with pytest.raises(errors.AuthorizationError): self.handler.handle_authorizations(mock_order, self.mock_config) assert self.mock_auth.cleanup.call_count == 1 assert self.mock_auth.cleanup.call_args[0][0][0].typ == "http-01" def test_incomplete_authzr_error(self): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) self.mock_net.poll.side_effect = _gen_mock_on_poll(status=messages.STATUS_INVALID) with test_util.patch_display_util(): with pytest.raises(errors.AuthorizationError, match='Some challenges have failed.'): self.handler.handle_authorizations(mock_order, self.mock_config, False) assert self.mock_auth.cleanup.call_count == 1 assert self.mock_auth.cleanup.call_args[0][0][0].typ == "http-01" def test_best_effort(self): def _conditional_mock_on_poll(authzr): """This mock will invalidate one authzr, and invalidate the other one""" valid_mock = _gen_mock_on_poll(messages.STATUS_VALID) invalid_mock = _gen_mock_on_poll(messages.STATUS_INVALID) if authzr.body.identifier.value == 'will-be-invalid': return invalid_mock(authzr) return valid_mock(authzr) # Two authzrs. Only one will be valid. authzrs = [gen_dom_authzr(domain="will-be-valid", challs=acme_util.CHALLENGES), gen_dom_authzr(domain="will-be-invalid", challs=acme_util.CHALLENGES)] self.mock_net.poll.side_effect = _conditional_mock_on_poll mock_order = mock.MagicMock(authorizations=authzrs) with mock.patch('certbot._internal.auth_handler.AuthHandler._report_failed_authzrs') \ as mock_report: valid_authzr = self.handler.handle_authorizations(mock_order, self.mock_config, True) # Because best_effort=True, we did not blow up. Instead ... assert len(valid_authzr) == 1 # ... the valid authzr has been processed assert mock_report.call_count == 1 # ... the invalid authzr has been reported self.mock_net.poll.side_effect = _gen_mock_on_poll(status=messages.STATUS_INVALID) with test_util.patch_display_util(): with pytest.raises(errors.AuthorizationError, match='All challenges have failed.'): # Despite best_effort=True, process will fail because no authzr is valid. self.handler.handle_authorizations(mock_order, self.mock_config, True) def test_validated_challenge_not_rerun(self): # With a pending challenge that is not supported by the plugin, we # expect an exception to be raised. authzr = acme_util.gen_authzr( messages.STATUS_PENDING, "0", [acme_util.DNS01], [messages.STATUS_PENDING]) mock_order = mock.MagicMock(authorizations=[authzr]) with pytest.raises(errors.AuthorizationError): self.handler.handle_authorizations(mock_order, self.mock_config) # With a validated challenge that is not supported by the plugin, we # expect the challenge to not be solved again and # handle_authorizations() to succeed. authzr = acme_util.gen_authzr( messages.STATUS_VALID, "0", [acme_util.DNS01], [messages.STATUS_VALID]) mock_order = mock.MagicMock(authorizations=[authzr]) self.handler.handle_authorizations(mock_order, self.mock_config) def test_valid_authzrs_deactivated(self): """When we deactivate valid authzrs in an orderr, we expect them to become deactivated and to receive a list of deactivated authzrs in return.""" def _mock_deactivate(authzr): if authzr.body.status == messages.STATUS_VALID: if authzr.body.identifier.value == "is_valid_but_will_fail": raise acme_errors.Error("Mock deactivation ACME error") authzb = authzr.body.update(status=messages.STATUS_DEACTIVATED) authzr = messages.AuthorizationResource(body=authzb) else: # pragma: no cover raise errors.Error("Can't deactivate non-valid authz") return authzr to_deactivate = [("is_valid", messages.STATUS_VALID), ("is_pending", messages.STATUS_PENDING), ("is_valid_but_will_fail", messages.STATUS_VALID)] to_deactivate = [acme_util.gen_authzr(a[1], a[0], [acme_util.HTTP01], [a[1]]) for a in to_deactivate] orderr = mock.MagicMock(authorizations=to_deactivate) self.mock_net.deactivate_authorization.side_effect = _mock_deactivate authzrs, failed = self.handler.deactivate_valid_authorizations(orderr) assert self.mock_net.deactivate_authorization.call_count == 2 assert len(authzrs) == 1 assert len(failed) == 1 assert authzrs[0].body.identifier.value == "is_valid" assert authzrs[0].body.status == messages.STATUS_DEACTIVATED assert failed[0].body.identifier.value == "is_valid_but_will_fail" assert failed[0].body.status == messages.STATUS_VALID def _gen_mock_on_poll(status=messages.STATUS_VALID, retry=0, wait_value=1): state = {'count': retry} def _mock(authzr): state['count'] = state['count'] - 1 effective_status = status if state['count'] < 0 else messages.STATUS_PENDING updated_azr = acme_util.gen_authzr( effective_status, authzr.body.identifier.value, [challb.chall for challb in authzr.body.challenges], [effective_status] * len(authzr.body.challenges)) return updated_azr, mock.MagicMock(headers={'Retry-After': str(wait_value)}) return _mock class ChallbToAchallTest(unittest.TestCase): """Tests for certbot._internal.auth_handler.challb_to_achall.""" def _call(self, challb): from certbot._internal.auth_handler import challb_to_achall return challb_to_achall(challb, "account_key", "domain") def test_it(self): assert self._call(acme_util.HTTP01_P) == \ achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, account_key="account_key", domain="domain") class GenChallengePathTest(unittest.TestCase): """Tests for certbot._internal.auth_handler.gen_challenge_path. """ def setUp(self): logging.disable(logging.FATAL) def tearDown(self): logging.disable(logging.NOTSET) @classmethod def _call(cls, challbs, preferences): from certbot._internal.auth_handler import gen_challenge_path return gen_challenge_path(challbs, preferences) def test_common_case(self): """Given DNS01 and HTTP01 with appropriate combos.""" challbs = (acme_util.DNS01_P, acme_util.HTTP01_P) prefs = [challenges.DNS01, challenges.HTTP01] assert self._call(challbs, prefs) == (0,) assert self._call(challbs[::-1], prefs) == (1,) def test_not_supported(self): challbs = (acme_util.DNS01_P,) prefs = [challenges.HTTP01] # smart path fails because no challs in prefs satisfies combos with pytest.raises(errors.AuthorizationError): self._call(challbs, prefs) class ReportFailedAuthzrsTest(unittest.TestCase): """Tests for certbot._internal.auth_handler.AuthHandler._report_failed_authzrs.""" # pylint: disable=protected-access def setUp(self): from certbot._internal.auth_handler import AuthHandler self.mock_auth = mock.MagicMock(spec=plugin_common.Plugin, name="buzz") self.mock_auth.name = "buzz" self.mock_auth.auth_hint.return_value = "the buzz hint" self.handler = AuthHandler(self.mock_auth, mock.MagicMock(), mock.MagicMock(), []) kwargs = { "chall": acme_util.HTTP01, "uri": "uri", "status": messages.STATUS_INVALID, "error": messages.Error.with_code("tls", detail="detail"), } # Prevent future regressions if the error type changes assert kwargs["error"].description is not None http_01 = messages.ChallengeBody(**kwargs) kwargs["chall"] = acme_util.HTTP01 http_01 = messages.ChallengeBody(**kwargs) self.authzr1 = mock.MagicMock() self.authzr1.body.identifier.value = 'example.com' self.authzr1.body.challenges = [http_01, http_01] kwargs["error"] = messages.Error.with_code("dnssec", detail="detail") http_01_diff = messages.ChallengeBody(**kwargs) self.authzr2 = mock.MagicMock() self.authzr2.body.identifier.value = 'foo.bar' self.authzr2.body.challenges = [http_01_diff] @mock.patch('certbot._internal.auth_handler.display_util.notify') def test_same_error_and_domain(self, mock_notify): self.handler._report_failed_authzrs([self.authzr1]) mock_notify.assert_called_with( '\n' 'Certbot failed to authenticate some domains (authenticator: buzz). ' 'The Certificate Authority reported these problems:\n' ' Domain: example.com\n' ' Type: tls\n' ' Detail: detail\n' '\n' ' Domain: example.com\n' ' Type: tls\n' ' Detail: detail\n' '\nHint: the buzz hint\n' ) @mock.patch('certbot._internal.auth_handler.display_util.notify') def test_different_errors_and_domains(self, mock_notify): self.mock_auth.name = "quux" self.mock_auth.auth_hint.return_value = "quuuuuux" self.handler._report_failed_authzrs([self.authzr1, self.authzr2]) mock_notify.assert_called_with( '\n' 'Certbot failed to authenticate some domains (authenticator: quux). ' 'The Certificate Authority reported these problems:\n' ' Domain: foo.bar\n' ' Type: dnssec\n' ' Detail: detail\n' '\n' ' Domain: example.com\n' ' Type: tls\n' ' Detail: detail\n' '\n' ' Domain: example.com\n' ' Type: tls\n' ' Detail: detail\n' '\nHint: quuuuuux\n' ) @mock.patch('certbot._internal.auth_handler.display_util.notify') def test_non_subclassed_authenticator(self, mock_notify): """If authenticator not derived from common.Plugin, we shouldn't call .auth_hint""" from certbot._internal.auth_handler import AuthHandler self.mock_auth = mock.MagicMock(name="quuz") self.mock_auth.name = "quuz" self.mock_auth.auth_hint.side_effect = Exception self.handler = AuthHandler(self.mock_auth, mock.MagicMock(), mock.MagicMock(), []) self.handler._report_failed_authzrs([self.authzr1]) assert mock_notify.call_count == 1 def gen_auth_resp(chall_list): """Generate a dummy authorization response.""" return ["%s%s" % (chall.__class__.__name__, chall.domain) for chall in chall_list] def gen_dom_authzr(domain, challs): """Generates new authzr for domains.""" return acme_util.gen_authzr( messages.STATUS_PENDING, domain, challs, [messages.STATUS_PENDING] * len(challs)) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/cert_manager_test.py0000664000175100017510000007461314561227515023022 0ustar00ericaerica """Tests for certbot._internal.cert_manager.""" # pylint: disable=protected-access import re import shutil import sys import tempfile import unittest from unittest import mock import configobj import pytest from certbot import configuration from certbot import errors from certbot._internal.storage import ALL_FOUR from certbot._internal.tests import storage_test from certbot.compat import filesystem from certbot.compat import os from certbot.display import util as display_util from certbot.tests import util as test_util class BaseCertManagerTest(test_util.ConfigTestCase): """Base class for setting up Cert Manager tests. """ def setUp(self): super().setUp() self.config.quiet = False filesystem.makedirs(self.config.renewal_configs_dir) self.domains = { "example.org": None, "other.com": os.path.join(self.config.config_dir, "specialarchive") } self.config_files = {domain: self._set_up_config(domain, self.domains[domain]) for domain in self.domains} # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. with open(os.path.join(self.config.renewal_configs_dir, "IGNORE.THIS"), "w") as junk: junk.write("This file should be ignored!") def _set_up_config(self, domain, custom_archive): # TODO: maybe provide NamespaceConfig.make_dirs? # TODO: main() should create those dirs, c.f. #902 filesystem.makedirs(os.path.join(self.config.live_dir, domain)) config_file = configobj.ConfigObj() if custom_archive is not None: filesystem.makedirs(custom_archive) config_file["archive_dir"] = custom_archive else: filesystem.makedirs(os.path.join(self.config.default_archive_dir, domain)) for kind in ALL_FOUR: config_file[kind] = os.path.join(self.config.live_dir, domain, kind + ".pem") config_file.filename = os.path.join(self.config.renewal_configs_dir, domain + ".conf") config_file.write() return config_file class UpdateLiveSymlinksTest(BaseCertManagerTest): """Tests for certbot._internal.cert_manager.update_live_symlinks """ def test_update_live_symlinks(self): """Test update_live_symlinks""" # create files with incorrect symlinks from certbot._internal import cert_manager archive_paths = {} for domain in self.domains: custom_archive = self.domains[domain] if custom_archive is not None: archive_dir_path = custom_archive else: archive_dir_path = os.path.join(self.config.default_archive_dir, domain) archive_paths[domain] = {kind: os.path.join(archive_dir_path, kind + "1.pem") for kind in ALL_FOUR} for kind in ALL_FOUR: live_path = self.config_files[domain][kind] archive_path = archive_paths[domain][kind] open(archive_path, 'a').close() # path is incorrect but base must be correct os.symlink(os.path.join(self.config.config_dir, kind + "1.pem"), live_path) # run update symlinks cert_manager.update_live_symlinks(self.config) # check that symlinks go where they should prev_dir = os.getcwd() try: for domain in self.domains: for kind in ALL_FOUR: os.chdir(os.path.dirname(self.config_files[domain][kind])) assert filesystem.realpath(filesystem.readlink(self.config_files[domain][kind])) == \ filesystem.realpath(archive_paths[domain][kind]) finally: os.chdir(prev_dir) class DeleteTest(storage_test.BaseRenewableCertTest): """Tests for certbot._internal.cert_manager.delete """ def _call(self): from certbot._internal import cert_manager cert_manager.delete(self.config) @test_util.patch_display_util() @mock.patch('certbot.display.util.notify') @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.storage.delete_files') def test_delete_from_config_yes(self, mock_delete_files, mock_lineage_for_certname, mock_notify, mock_util): """Test delete""" mock_lineage_for_certname.return_value = self.test_rc mock_util().yesno.return_value = True self.config.certname = "example.org" self._call() mock_delete_files.assert_called_once_with(self.config, "example.org") mock_notify.assert_called_once_with( "Deleted all files relating to certificate example.org." ) @test_util.patch_display_util() @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.storage.delete_files') def test_delete_from_config_no(self, mock_delete_files, mock_lineage_for_certname, mock_util): """Test delete""" mock_lineage_for_certname.return_value = self.test_rc mock_util().yesno.return_value = False self.config.certname = "example.org" self._call() assert mock_delete_files.call_count == 0 @test_util.patch_display_util() @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.storage.delete_files') def test_delete_interactive_single_yes(self, mock_delete_files, mock_lineage_for_certname, mock_util): """Test delete""" mock_lineage_for_certname.return_value = self.test_rc mock_util().checklist.return_value = (display_util.OK, ["example.org"]) mock_util().yesno.return_value = True self._call() mock_delete_files.assert_called_once_with(self.config, "example.org") @test_util.patch_display_util() @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.storage.delete_files') def test_delete_interactive_single_no(self, mock_delete_files, mock_lineage_for_certname, mock_util): """Test delete""" mock_lineage_for_certname.return_value = self.test_rc mock_util().checklist.return_value = (display_util.OK, ["example.org"]) mock_util().yesno.return_value = False self._call() assert mock_delete_files.call_count == 0 @test_util.patch_display_util() @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.storage.delete_files') def test_delete_interactive_multiple_yes(self, mock_delete_files, mock_lineage_for_certname, mock_util): """Test delete""" mock_lineage_for_certname.return_value = self.test_rc mock_util().checklist.return_value = (display_util.OK, ["example.org", "other.org"]) mock_util().yesno.return_value = True self._call() mock_delete_files.assert_any_call(self.config, "example.org") mock_delete_files.assert_any_call(self.config, "other.org") assert mock_delete_files.call_count == 2 @test_util.patch_display_util() @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.storage.delete_files') def test_delete_interactive_multiple_no(self, mock_delete_files, mock_lineage_for_certname, mock_util): """Test delete""" mock_lineage_for_certname.return_value = self.test_rc mock_util().checklist.return_value = (display_util.OK, ["example.org", "other.org"]) mock_util().yesno.return_value = False self._call() assert mock_delete_files.call_count == 0 class CertificatesTest(BaseCertManagerTest): """Tests for certbot._internal.cert_manager.certificates """ def _certificates(self, *args, **kwargs): from certbot._internal.cert_manager import certificates return certificates(*args, **kwargs) @mock.patch('certbot._internal.cert_manager.logger') @test_util.patch_display_util() def test_certificates_parse_fail(self, mock_utility, mock_logger): self._certificates(self.config) assert mock_logger.warning.called #pylint: disable=no-member assert mock_utility.called @mock.patch('certbot._internal.cert_manager.logger') @test_util.patch_display_util() def test_certificates_quiet(self, mock_utility, mock_logger): self.config.quiet = True self._certificates(self.config) assert mock_utility.notification.called is False assert mock_logger.warning.called #pylint: disable=no-member @mock.patch('certbot.crypto_util.verify_renewable_cert') @mock.patch('certbot._internal.cert_manager.logger') @test_util.patch_display_util() @mock.patch("certbot._internal.storage.RenewableCert") @mock.patch('certbot._internal.cert_manager._report_human_readable') def test_certificates_parse_success(self, mock_report, mock_renewable_cert, mock_utility, mock_logger, mock_verifier): mock_verifier.return_value = None mock_report.return_value = "" self._certificates(self.config) assert mock_logger.warning.called is False assert mock_report.called assert mock_utility.called assert mock_renewable_cert.called @mock.patch('certbot._internal.cert_manager.logger') @test_util.patch_display_util() def test_certificates_no_files(self, mock_utility, mock_logger): empty_tempdir = tempfile.mkdtemp() empty_config = configuration.NamespaceConfig(mock.MagicMock( config_dir=os.path.join(empty_tempdir, "config"), work_dir=os.path.join(empty_tempdir, "work"), logs_dir=os.path.join(empty_tempdir, "logs"), quiet=False )) filesystem.makedirs(empty_config.renewal_configs_dir) self._certificates(empty_config) assert mock_logger.warning.called is False assert mock_utility.called shutil.rmtree(empty_tempdir) @mock.patch('certbot.crypto_util.get_serial_from_cert') @mock.patch('certbot._internal.cert_manager.ocsp.RevocationChecker.ocsp_revoked') def test_report_human_readable(self, mock_revoked, mock_serial): mock_revoked.return_value = None mock_serial.return_value = 1234567890 import datetime import pytz from certbot._internal import cert_manager expiry = datetime.datetime.now(pytz.UTC) cert = mock.MagicMock(lineagename="nameone") cert.target_expiry = expiry cert.names.return_value = ["nameone", "nametwo"] cert.is_test_cert = False parsed_certs = [cert] mock_config = mock.MagicMock(certname=None, lineagename=None) # pylint: disable=protected-access # pylint: disable=protected-access get_report = lambda: cert_manager._report_human_readable(mock_config, parsed_certs) out = get_report() assert "INVALID: EXPIRED" in out cert.target_expiry += datetime.timedelta(hours=2) # pylint: disable=protected-access out = get_report() assert ('1 hour' in out or '2 hour(s)' in out) is True assert 'VALID' in out assert 'INVALID' not in out cert.target_expiry += datetime.timedelta(days=1) # pylint: disable=protected-access out = get_report() assert '1 day' in out assert 'under' not in out assert 'VALID' in out assert 'INVALID' not in out cert.target_expiry += datetime.timedelta(days=2) # pylint: disable=protected-access out = get_report() assert '3 days' in out assert 'VALID' in out assert 'INVALID' not in out cert.is_test_cert = True mock_revoked.return_value = True out = get_report() assert 'INVALID: TEST_CERT, REVOKED' in out cert = mock.MagicMock(lineagename="indescribable") cert.target_expiry = expiry cert.names.return_value = ["nameone", "thrice.named"] cert.is_test_cert = True parsed_certs.append(cert) out = get_report() assert len(re.findall("INVALID:", out)) == 2 mock_config.domains = ["thrice.named"] out = get_report() assert len(re.findall("INVALID:", out)) == 1 mock_config.domains = ["nameone"] out = get_report() assert len(re.findall("INVALID:", out)) == 2 mock_config.certname = "indescribable" out = get_report() assert len(re.findall("INVALID:", out)) == 1 mock_config.certname = "horror" out = get_report() assert len(re.findall("INVALID:", out)) == 0 class SearchLineagesTest(BaseCertManagerTest): """Tests for certbot._internal.cert_manager._search_lineages.""" @mock.patch('certbot.util.make_or_verify_dir') @mock.patch('certbot._internal.storage.renewal_conf_files') @mock.patch('certbot._internal.storage.RenewableCert') def test_cert_storage_error(self, mock_renewable_cert, mock_renewal_conf_files, mock_make_or_verify_dir): mock_renewal_conf_files.return_value = ["badfile"] mock_renewable_cert.side_effect = errors.CertStorageError from certbot._internal import cert_manager # pylint: disable=protected-access assert cert_manager._search_lineages(self.config, lambda x: x, "check") == "check" assert mock_make_or_verify_dir.called class LineageForCertnameTest(BaseCertManagerTest): """Tests for certbot._internal.cert_manager.lineage_for_certname""" @mock.patch('certbot.util.make_or_verify_dir') @mock.patch('certbot._internal.storage.renewal_file_for_certname') @mock.patch('certbot._internal.storage.RenewableCert') def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file, mock_make_or_verify_dir): mock_renewal_conf_file.return_value = "somefile.conf" mock_match = mock.Mock(lineagename="example.com") mock_renewable_cert.return_value = mock_match from certbot._internal import cert_manager assert cert_manager.lineage_for_certname(self.config, "example.com") == mock_match assert mock_make_or_verify_dir.called @mock.patch('certbot.util.make_or_verify_dir') @mock.patch('certbot._internal.storage.renewal_file_for_certname') def test_no_match(self, mock_renewal_conf_file, mock_make_or_verify_dir): mock_renewal_conf_file.return_value = "other.com.conf" from certbot._internal import cert_manager assert cert_manager.lineage_for_certname(self.config, "example.com") is None assert mock_make_or_verify_dir.called @mock.patch('certbot.util.make_or_verify_dir') @mock.patch('certbot._internal.storage.renewal_file_for_certname') def test_no_renewal_file(self, mock_renewal_conf_file, mock_make_or_verify_dir): mock_renewal_conf_file.side_effect = errors.CertStorageError() from certbot._internal import cert_manager assert cert_manager.lineage_for_certname(self.config, "example.com") is None assert mock_make_or_verify_dir.called class DomainsForCertnameTest(BaseCertManagerTest): """Tests for certbot._internal.cert_manager.domains_for_certname""" @mock.patch('certbot.util.make_or_verify_dir') @mock.patch('certbot._internal.storage.renewal_file_for_certname') @mock.patch('certbot._internal.storage.RenewableCert') def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file, mock_make_or_verify_dir): mock_renewal_conf_file.return_value = "somefile.conf" mock_match = mock.Mock(lineagename="example.com") domains = ["example.com", "example.org"] mock_match.names.return_value = domains mock_renewable_cert.return_value = mock_match from certbot._internal import cert_manager assert cert_manager.domains_for_certname(self.config, "example.com") == \ domains assert mock_make_or_verify_dir.called @mock.patch('certbot.util.make_or_verify_dir') @mock.patch('certbot._internal.storage.renewal_file_for_certname') def test_no_match(self, mock_renewal_conf_file, mock_make_or_verify_dir): mock_renewal_conf_file.return_value = "somefile.conf" from certbot._internal import cert_manager assert cert_manager.domains_for_certname(self.config, "other.com") is None assert mock_make_or_verify_dir.called class RenameLineageTest(BaseCertManagerTest): """Tests for certbot._internal.cert_manager.rename_lineage""" def setUp(self): super().setUp() self.config.certname = "example.org" self.config.new_certname = "after" def _call(self, *args, **kwargs): from certbot._internal import cert_manager return cert_manager.rename_lineage(*args, **kwargs) @mock.patch('certbot._internal.storage.renewal_conf_files') @test_util.patch_display_util() def test_no_certname(self, mock_get_utility, mock_renewal_conf_files): self.config.certname = None self.config.new_certname = "two" # if not choices mock_renewal_conf_files.return_value = [] with pytest.raises(errors.Error): self._call(self.config) mock_renewal_conf_files.return_value = ["one.conf"] util_mock = mock_get_utility() util_mock.menu.return_value = (display_util.CANCEL, 0) with pytest.raises(errors.Error): self._call(self.config) util_mock.menu.return_value = (display_util.OK, -1) with pytest.raises(errors.Error): self._call(self.config) @test_util.patch_display_util() def test_no_new_certname(self, mock_get_utility): self.config.certname = "one" self.config.new_certname = None util_mock = mock_get_utility() util_mock.input.return_value = (display_util.CANCEL, "name") with pytest.raises(errors.Error): self._call(self.config) util_mock.input.return_value = (display_util.OK, None) with pytest.raises(errors.Error): self._call(self.config) @test_util.patch_display_util() @mock.patch('certbot._internal.cert_manager.lineage_for_certname') def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utility): self.config.certname = "one" self.config.new_certname = "two" mock_lineage_for_certname.return_value = None with pytest.raises(errors.ConfigurationError): self._call(self.config) @test_util.patch_display_util() @mock.patch("certbot._internal.storage.RenewableCert._check_symlinks") def test_rename_cert(self, mock_check, unused_get_utility): mock_check.return_value = True self._call(self.config) from certbot._internal import cert_manager updated_lineage = cert_manager.lineage_for_certname(self.config, self.config.new_certname) assert updated_lineage is not None assert updated_lineage.lineagename == self.config.new_certname @test_util.patch_display_util() @mock.patch("certbot._internal.storage.RenewableCert._check_symlinks") def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility): mock_check.return_value = True self.config.certname = None util_mock = mock_get_utility() util_mock.menu.return_value = (display_util.OK, 0) self._call(self.config) from certbot._internal import cert_manager updated_lineage = cert_manager.lineage_for_certname(self.config, self.config.new_certname) assert updated_lineage is not None assert updated_lineage.lineagename == self.config.new_certname @test_util.patch_display_util() @mock.patch("certbot._internal.storage.RenewableCert._check_symlinks") def test_rename_cert_bad_new_certname(self, mock_check, unused_get_utility): mock_check.return_value = True # for example, don't rename to existing certname self.config.new_certname = "example.org" with pytest.raises(errors.ConfigurationError): self._call(self.config) self.config.new_certname = "one{0}two".format(os.path.sep) with pytest.raises(errors.ConfigurationError): self._call(self.config) class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): """Test to avoid duplicate lineages.""" def setUp(self): super().setUp() self.config_file.write() self._write_out_ex_kinds() @mock.patch('certbot.util.make_or_verify_dir') def test_find_duplicative_names(self, unused_makedir): from certbot._internal.cert_manager import find_duplicative_certs test_cert = test_util.load_vector('cert-san_512.pem') with open(self.test_rc.cert, 'wb') as f: f.write(test_cert) # No overlap at all result = find_duplicative_certs( self.config, ['wow.net', 'hooray.org']) assert result == (None, None) # Totally identical result = find_duplicative_certs( self.config, ['example.com', 'www.example.com']) assert result[0].configfile.filename.endswith('example.org.conf') assert result[1] is None # Superset result = find_duplicative_certs( self.config, ['example.com', 'www.example.com', 'something.new']) assert result[0] is None assert result[1].configfile.filename.endswith('example.org.conf') # Partial overlap doesn't count result = find_duplicative_certs( self.config, ['example.com', 'something.new']) assert result == (None, None) class CertPathToLineageTest(storage_test.BaseRenewableCertTest): """Tests for certbot._internal.cert_manager.cert_path_to_lineage""" def setUp(self): super().setUp() self.config_file.write() self._write_out_ex_kinds() self.fullchain = os.path.join(self.config.config_dir, 'live', 'example.org', 'fullchain.pem') self.config.cert_path = self.fullchain def _call(self, cli_config): from certbot._internal.cert_manager import cert_path_to_lineage return cert_path_to_lineage(cli_config) def _archive_files(self, cli_config, filetype): from certbot._internal.cert_manager import _archive_files return _archive_files(cli_config, filetype) def test_basic_match(self): assert 'example.org' == self._call(self.config) def test_no_match_exists(self): bad_test_config = self.config bad_test_config.cert_path = os.path.join(self.config.config_dir, 'live', 'SailorMoon', 'fullchain.pem') with pytest.raises(errors.Error): self._call(bad_test_config) @mock.patch('certbot._internal.cert_manager._acceptable_matches') def test_options_fullchain(self, mock_acceptable_matches): mock_acceptable_matches.return_value = [lambda x: x.fullchain_path] self.config.fullchain_path = self.fullchain assert 'example.org' == self._call(self.config) @mock.patch('certbot._internal.cert_manager._acceptable_matches') def test_options_cert_path(self, mock_acceptable_matches): mock_acceptable_matches.return_value = [lambda x: x.cert_path] test_cert_path = os.path.join(self.config.config_dir, 'live', 'example.org', 'cert.pem') self.config.cert_path = test_cert_path assert 'example.org' == self._call(self.config) @mock.patch('certbot._internal.cert_manager._acceptable_matches') def test_options_archive_cert(self, mock_acceptable_matches): # Also this and the next test check that the regex of _archive_files is working. self.config.cert_path = os.path.join(self.config.config_dir, 'archive', 'example.org', 'cert11.pem') mock_acceptable_matches.return_value = [lambda x: self._archive_files(x, 'cert')] assert 'example.org' == self._call(self.config) @mock.patch('certbot._internal.cert_manager._acceptable_matches') def test_options_archive_fullchain(self, mock_acceptable_matches): self.config.cert_path = os.path.join(self.config.config_dir, 'archive', 'example.org', 'fullchain11.pem') mock_acceptable_matches.return_value = [lambda x: self._archive_files(x, 'fullchain')] assert 'example.org' == self._call(self.config) def test_only_path(self): self.config.cert_path = self.fullchain assert 'example.org' == self._call(self.config) class MatchAndCheckOverlaps(storage_test.BaseRenewableCertTest): """Tests for certbot._internal.cert_manager.match_and_check_overlaps w/o overlapping archive dirs.""" # A test with real overlapping archive dirs can be found in tests/boulder_integration.sh def setUp(self): super().setUp() self.config_file.write() self._write_out_ex_kinds() self.fullchain = os.path.join(self.config.config_dir, 'live', 'example.org', 'fullchain.pem') self.config.cert_path = self.fullchain def _call(self, cli_config, acceptable_matches, match_func, rv_func): from certbot._internal.cert_manager import match_and_check_overlaps return match_and_check_overlaps(cli_config, acceptable_matches, match_func, rv_func) def test_basic_match(self): from certbot._internal.cert_manager import _acceptable_matches assert ['example.org'] == self._call(self.config, _acceptable_matches(), lambda x: self.config.cert_path, lambda x: x.lineagename) @mock.patch('certbot._internal.cert_manager._search_lineages') def test_no_matches(self, mock_search_lineages): mock_search_lineages.return_value = [] with pytest.raises(errors.Error): self._call(self.config, None, None, None) @mock.patch('certbot._internal.cert_manager._search_lineages') def test_too_many_matches(self, mock_search_lineages): mock_search_lineages.return_value = ['spider', 'dance'] with pytest.raises(errors.OverlappingMatchFound): self._call(self.config, None, None, None) class GetCertnameTest(unittest.TestCase): """Tests for certbot._internal.cert_manager.""" def setUp(self): get_utility_patch = test_util.patch_display_util() self.mock_get_utility = get_utility_patch.start() self.addCleanup(get_utility_patch.stop) self.config = mock.MagicMock() self.config.certname = None @mock.patch('certbot._internal.storage.renewal_conf_files') @mock.patch('certbot._internal.storage.lineagename_for_filename') def test_get_certnames(self, mock_name, mock_files): mock_files.return_value = ['example.com.conf'] mock_name.return_value = 'example.com' from certbot._internal import cert_manager prompt = "Which certificate would you" self.mock_get_utility().menu.return_value = (display_util.OK, 0) assert cert_manager.get_certnames( self.config, "verb", allow_multiple=False) == ['example.com'] assert prompt in self.mock_get_utility().menu.call_args[0][0] @mock.patch('certbot._internal.storage.renewal_conf_files') @mock.patch('certbot._internal.storage.lineagename_for_filename') def test_get_certnames_custom_prompt(self, mock_name, mock_files): mock_files.return_value = ['example.com.conf'] mock_name.return_value = 'example.com' from certbot._internal import cert_manager prompt = "custom prompt" self.mock_get_utility().menu.return_value = (display_util.OK, 0) assert cert_manager.get_certnames( self.config, "verb", allow_multiple=False, custom_prompt=prompt) == \ ['example.com'] assert self.mock_get_utility().menu.call_args[0][0] == \ prompt @mock.patch('certbot._internal.storage.renewal_conf_files') @mock.patch('certbot._internal.storage.lineagename_for_filename') def test_get_certnames_user_abort(self, mock_name, mock_files): mock_files.return_value = ['example.com.conf'] mock_name.return_value = 'example.com' from certbot._internal import cert_manager self.mock_get_utility().menu.return_value = (display_util.CANCEL, 0) with pytest.raises(errors.Error): cert_manager.get_certnames(self.config, "erroring_anyway", allow_multiple=False) @mock.patch('certbot._internal.storage.renewal_conf_files') @mock.patch('certbot._internal.storage.lineagename_for_filename') def test_get_certnames_allow_multiple(self, mock_name, mock_files): mock_files.return_value = ['example.com.conf'] mock_name.return_value = 'example.com' from certbot._internal import cert_manager prompt = "Which certificate(s) would you" self.mock_get_utility().checklist.return_value = (display_util.OK, ['example.com']) assert cert_manager.get_certnames( self.config, "verb", allow_multiple=True) == ['example.com'] assert prompt in self.mock_get_utility().checklist.call_args[0][0] @mock.patch('certbot._internal.storage.renewal_conf_files') @mock.patch('certbot._internal.storage.lineagename_for_filename') def test_get_certnames_allow_multiple_custom_prompt(self, mock_name, mock_files): mock_files.return_value = ['example.com.conf'] mock_name.return_value = 'example.com' from certbot._internal import cert_manager prompt = "custom prompt" self.mock_get_utility().checklist.return_value = (display_util.OK, ['example.com']) assert cert_manager.get_certnames( self.config, "verb", allow_multiple=True, custom_prompt=prompt) == \ ['example.com'] assert self.mock_get_utility().checklist.call_args[0][0] == \ prompt @mock.patch('certbot._internal.storage.renewal_conf_files') @mock.patch('certbot._internal.storage.lineagename_for_filename') def test_get_certnames_allow_multiple_user_abort(self, mock_name, mock_files): mock_files.return_value = ['example.com.conf'] mock_name.return_value = 'example.com' from certbot._internal import cert_manager self.mock_get_utility().checklist.return_value = (display_util.CANCEL, []) with pytest.raises(errors.Error): cert_manager.get_certnames(self.config, "erroring_anyway", allow_multiple=True) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/cli_test.py0000664000175100017510000006314414561227515021137 0ustar00ericaerica"""Tests for certbot._internal.cli.""" import argparse import copy from importlib import reload as reload_module import io import sys import tempfile from typing import Any, List import unittest from unittest import mock import pytest from acme import challenges from certbot import errors from certbot.configuration import ArgumentSource, NamespaceConfig from certbot._internal import cli from certbot._internal import constants from certbot._internal.cli.cli_utils import flag_default from certbot._internal.plugins import disco from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util from certbot.tests.util import TempDirTestCase PLUGINS = disco.PluginsRegistry.find_all() class TestReadFile(TempDirTestCase): """Test cli.read_file""" def test_read_file(self): curr_dir = os.getcwd() try: # On Windows current directory may be on a different drive than self.tempdir. # However a relative path between two different drives is invalid. So we move to # self.tempdir to ensure that we stay on the same drive. os.chdir(self.tempdir) # The read-only filesystem introduced with macOS Catalina can break # code using relative paths below. See # https://bugs.python.org/issue38295 for another example of this. # Eliminating any possible symlinks in self.tempdir before passing # it to os.path.relpath solves the problem. This is done by calling # filesystem.realpath which removes any symlinks in the path on # POSIX systems. real_path = filesystem.realpath(os.path.join(self.tempdir, 'foo')) relative_path = os.path.relpath(real_path) with pytest.raises(argparse.ArgumentTypeError): cli.read_file(relative_path) test_contents = b'bar\n' with open(relative_path, 'wb') as f: f.write(test_contents) path, contents = cli.read_file(relative_path) assert path == os.path.abspath(path) assert contents == test_contents finally: os.chdir(curr_dir) class FlagDefaultTest(unittest.TestCase): """Tests cli.flag_default""" def test_default_directories(self): if os.name != 'nt': assert cli.flag_default('config_dir') == '/etc/letsencrypt' assert cli.flag_default('work_dir') == '/var/lib/letsencrypt' assert cli.flag_default('logs_dir') == '/var/log/letsencrypt' else: assert cli.flag_default('config_dir') == 'C:\\Certbot' assert cli.flag_default('work_dir') == 'C:\\Certbot\\lib' assert cli.flag_default('logs_dir') == 'C:\\Certbot\\log' def assert_set_by_user_with_value(namespace, attr: str, value: Any): assert getattr(namespace, attr) == value assert namespace.set_by_user(attr) def assert_value_and_source(namespace, attr: str, value: Any, source: ArgumentSource): assert getattr(namespace, attr) == value assert namespace.argument_sources[attr] == source class ParseTest(unittest.TestCase): '''Test the cli args entrypoint''' @staticmethod def _unmocked_parse(args: List[str]) -> NamespaceConfig: """Get result of cli.prepare_and_parse_args.""" return cli.prepare_and_parse_args(PLUGINS, args) @staticmethod def parse(args: List[str]) -> NamespaceConfig: """Mocks certbot._internal.display.obj.get_display and calls _unmocked_parse.""" with test_util.patch_display_util(): return ParseTest._unmocked_parse(args) def _help_output(self, args: List[str]): "Run a command, and return the output string for scrutiny" output = io.StringIO() def write_msg(message, *args, **kwargs): # pylint: disable=missing-docstring,unused-argument output.write(message) with mock.patch('certbot._internal.main.sys.stdout', new=output): with test_util.patch_display_util() as mock_get_utility: mock_get_utility().notification.side_effect = write_msg with mock.patch('certbot._internal.main.sys.stderr'): with pytest.raises(SystemExit): self._unmocked_parse(args) return output.getvalue() @mock.patch("certbot._internal.cli.helpful.flag_default") def test_cli_ini_domains(self, mock_flag_default): with tempfile.NamedTemporaryFile() as tmp_config: tmp_config.close() # close now because of compatibility issues on Windows # use a shim to get ConfigArgParse to pick up tmp_config shim = ( lambda v: copy.deepcopy(constants.CLI_DEFAULTS[v]) if v != "config_files" else [tmp_config.name] ) mock_flag_default.side_effect = shim namespace = self.parse(["certonly"]) assert_value_and_source(namespace, 'domains', [], ArgumentSource.DEFAULT) with open(tmp_config.name, 'w') as file_h: file_h.write("domains = example.com") namespace = self.parse(["certonly"]) assert_value_and_source(namespace, 'domains', ["example.com"], ArgumentSource.CONFIG_FILE) namespace = self.parse(["renew"]) assert_value_and_source(namespace, 'domains', [], ArgumentSource.RUNTIME) def test_no_args(self): namespace = self.parse([]) for d in ('config_dir', 'logs_dir', 'work_dir'): assert getattr(namespace, d) == cli.flag_default(d) assert not namespace.set_by_user(d) def test_install_abspath(self): cert = 'cert' key = 'key' chain = 'chain' fullchain = 'fullchain' with mock.patch('certbot._internal.main.install'): namespace = self.parse(['install', '--cert-path', cert, '--key-path', 'key', '--chain-path', 'chain', '--fullchain-path', 'fullchain']) assert namespace.cert_path == os.path.abspath(cert) assert namespace.key_path == os.path.abspath(key) assert namespace.chain_path == os.path.abspath(chain) assert namespace.fullchain_path == os.path.abspath(fullchain) def test_help(self): self._help_output(['--help']) # assert SystemExit is raised here out = self._help_output(['--help', 'all']) assert "--configurator" in out assert "how a certificate is deployed" in out assert "--webroot-path" in out assert "--text" not in out assert "%s" not in out assert "{0}" not in out assert "--renew-hook" not in out out = self._help_output(['-h', 'nginx']) if "nginx" in PLUGINS: # may be false while building distributions without plugins assert "--nginx-ctl" in out assert "--webroot-path" not in out assert "--checkpoints" not in out out = self._help_output(['-h']) if "nginx" in PLUGINS: assert "Use the Nginx plugin" in out else: assert "(the certbot nginx plugin is not" in out out = self._help_output(['--help', 'plugins']) assert "--webroot-path" not in out assert "--prepare" in out assert '"plugins" subcommand' in out # test multiple topics out = self._help_output(['-h', 'renew']) assert "--keep" in out out = self._help_output(['-h', 'automation']) assert "--keep" in out out = self._help_output(['-h', 'revoke']) assert "--keep" not in out out = self._help_output(['--help', 'install']) assert "--cert-path" in out assert "--key-path" in out out = self._help_output(['--help', 'revoke']) assert "--cert-path" in out assert "--key-path" in out assert "--reason" in out assert "--delete-after-revoke" in out assert "--no-delete-after-revoke" in out out = self._help_output(['-h', 'register']) assert "--cert-path" not in out assert "--key-path" not in out out = self._help_output(['-h']) assert cli.SHORT_USAGE in out assert cli.COMMAND_OVERVIEW[:100] in out assert "%s" not in out assert "{0}" not in out def test_help_no_dashes(self): self._help_output(['help']) # assert SystemExit is raised here out = self._help_output(['help', 'all']) assert "--configurator" in out assert "how a certificate is deployed" in out assert "--webroot-path" in out assert "--text" not in out assert "%s" not in out assert "{0}" not in out out = self._help_output(['help', 'install']) assert "--cert-path" in out assert "--key-path" in out out = self._help_output(['help', 'revoke']) assert "--cert-path" in out assert "--key-path" in out def test_parse_domains(self): short_args = ['-d', 'example.com'] namespace = self.parse(short_args) assert_set_by_user_with_value(namespace, 'domains', ['example.com']) short_args = ['-d', 'trailing.period.com.'] namespace = self.parse(short_args) assert_set_by_user_with_value(namespace, 'domains', ['trailing.period.com']) short_args = ['-d', 'example.com,another.net,third.org,example.com'] namespace = self.parse(short_args) assert_set_by_user_with_value(namespace, 'domains', ['example.com', 'another.net', 'third.org']) long_args = ['--domains', 'example.com'] namespace = self.parse(long_args) assert_set_by_user_with_value(namespace, 'domains', ['example.com']) long_args = ['--domains', 'trailing.period.com.'] namespace = self.parse(long_args) assert_set_by_user_with_value(namespace, 'domains', ['trailing.period.com']) long_args = ['--domains', 'example.com,another.net,example.com'] namespace = self.parse(long_args) assert_set_by_user_with_value(namespace, 'domains', ['example.com', 'another.net']) def test_preferred_challenges(self): short_args = ['--preferred-challenges', 'http, dns'] namespace = self.parse(short_args) expected = [challenges.HTTP01.typ, challenges.DNS01.typ] assert_set_by_user_with_value(namespace, 'pref_challs', expected) short_args = ['--preferred-challenges', 'jumping-over-the-moon'] # argparse.ArgumentError makes argparse print more information # to stderr and call sys.exit() with mock.patch('sys.stderr'): with pytest.raises(SystemExit): self.parse(short_args) def test_server_flag(self): namespace = self.parse('--server example.com'.split()) assert_set_by_user_with_value(namespace, 'server', 'example.com') def test_must_staple_flag(self): namespace = self.parse(['--must-staple']) assert_set_by_user_with_value(namespace, 'must_staple', True) assert_value_and_source(namespace, 'staple', True, ArgumentSource.RUNTIME) def test_must_staple_and_staple_ocsp_flags(self): namespace = self.parse(['--must-staple', '--staple-ocsp']) assert_set_by_user_with_value(namespace, 'must_staple', True) assert_set_by_user_with_value(namespace, 'staple', True) def _check_server_conflict_message(self, parser_args, conflicting_args): try: self.parse(parser_args) self.fail( # pragma: no cover "The following flags didn't conflict with " '--server: {0}'.format(', '.join(conflicting_args))) except errors.Error as error: assert '--server' in str(error) for arg in conflicting_args: assert arg in str(error) def test_staging_flag(self): short_args = ['--staging'] namespace = self.parse(short_args) assert_set_by_user_with_value(namespace, 'staging', True) assert_set_by_user_with_value(namespace, 'server', constants.STAGING_URI) short_args += '--server example.com'.split() self._check_server_conflict_message(short_args, '--staging') def _assert_dry_run_flag_worked(self, namespace, existing_account): assert_set_by_user_with_value(namespace, 'dry_run', True) assert_value_and_source(namespace, 'break_my_certs', True, ArgumentSource.RUNTIME) assert_value_and_source(namespace, 'staging', True, ArgumentSource.RUNTIME) assert_value_and_source(namespace, 'server', constants.STAGING_URI, ArgumentSource.RUNTIME) if existing_account: assert_value_and_source(namespace, 'tos', True, ArgumentSource.RUNTIME) assert_value_and_source(namespace, 'register_unsafely_without_email', True, ArgumentSource.RUNTIME) else: assert_value_and_source(namespace, 'tos', False, ArgumentSource.DEFAULT) assert_value_and_source(namespace, 'register_unsafely_without_email', False, ArgumentSource.DEFAULT) def test_dry_run_flag(self): config_dir = tempfile.mkdtemp() short_args = '--dry-run --config-dir {0}'.format(config_dir).split() with pytest.raises(errors.Error): self.parse(short_args) self._assert_dry_run_flag_worked( self.parse(short_args + ['auth']), False) self._assert_dry_run_flag_worked( self.parse(short_args + ['certonly']), False) self._assert_dry_run_flag_worked( self.parse(short_args + ['renew']), False) account_dir = os.path.join(config_dir, constants.ACCOUNTS_DIR) filesystem.mkdir(account_dir) filesystem.mkdir(os.path.join(account_dir, 'fake_account_dir')) self._assert_dry_run_flag_worked(self.parse(short_args + ['auth']), True) self._assert_dry_run_flag_worked(self.parse(short_args + ['renew']), True) self._assert_dry_run_flag_worked(self.parse(short_args + ['certonly']), True) short_args += ['certonly'] # `--dry-run --server example.com` should emit example.com config = self.parse(short_args + ['--server', 'example.com']) assert_set_by_user_with_value(config, 'server', 'example.com') # `--dry-run --server STAGING_URI` should emit STAGING_URI config = self.parse(short_args + ['--server', constants.STAGING_URI]) assert_set_by_user_with_value(config, 'server', constants.STAGING_URI) # `--dry-run --server LIVE` should emit STAGING_URI config = self.parse(short_args + ['--server', cli.flag_default("server")]) assert_value_and_source(config, 'server', constants.STAGING_URI, ArgumentSource.RUNTIME) # `--dry-run --server example.com --staging` should emit an error conflicts = ['--staging'] self._check_server_conflict_message(short_args + ['--server', 'example.com', '--staging'], conflicts) def test_user_set_rsa_key_size(self): key_size_option = 'rsa_key_size' key_size_value = cli.flag_default(key_size_option) config = self.parse('--rsa-key-size {0}'.format(key_size_value).split()) assert config.set_by_user(key_size_option) config_dir_option = 'config_dir' assert not config.set_by_user( config_dir_option) assert not config.set_by_user('authenticator') def test_user_set_installer_and_authenticator(self): config = self.parse('--apache') assert config.set_by_user('installer') assert config.set_by_user('authenticator') config = self.parse('--installer webroot') assert config.set_by_user('installer') assert not config.set_by_user('authenticator') def test_user_set_ecdsa_key_option(self): elliptic_curve_option = 'elliptic_curve' elliptic_curve_option_value = cli.flag_default(elliptic_curve_option) config = self.parse('--elliptic-curve {0}'.format(elliptic_curve_option_value).split()) assert config.set_by_user(elliptic_curve_option) def test_user_set_invalid_key_type(self): key_type_option = 'key_type' key_type_value = cli.flag_default(key_type_option) config = self.parse('--key-type {0}'.format(key_type_value).split()) assert config.set_by_user(key_type_option) with pytest.raises(SystemExit): self.parse("--key-type foo") @mock.patch('certbot._internal.hooks.validate_hooks') def test_user_set_deploy_hook(self, unused_mock_validate_hooks): args = 'renew --deploy-hook foo'.split() plugins = disco.PluginsRegistry.find_all() config = cli.prepare_and_parse_args(plugins, args) assert config.set_by_user('renew_hook') @mock.patch('certbot._internal.plugins.webroot._validate_webroot') def test_user_set_webroot_map(self, mock_validate_webroot): args = 'renew -w /var/www/html -d example.com'.split() mock_validate_webroot.return_value = '/var/www/html' plugins = disco.PluginsRegistry.find_all() config = cli.prepare_and_parse_args(plugins, args) assert config.set_by_user('webroot_map') def test_encode_revocation_reason(self): for reason, code in constants.REVOCATION_REASONS.items(): namespace = self.parse(['--reason', reason]) assert namespace.reason == code for reason, code in constants.REVOCATION_REASONS.items(): namespace = self.parse(['--reason', reason.upper()]) assert namespace.reason == code def test_force_interactive(self): with pytest.raises(errors.Error): self.parse("renew --force-interactive".split()) with pytest.raises(errors.Error): self.parse("-n --force-interactive".split()) def test_deploy_hook_conflict(self): with mock.patch("certbot._internal.cli.sys.stderr"): with pytest.raises(SystemExit): self.parse("--renew-hook foo --deploy-hook bar".split()) def test_deploy_hook_matches_renew_hook(self): value = "foo" namespace = self.parse(["--renew-hook", value, "--deploy-hook", value, "--disable-hook-validation"]) assert_set_by_user_with_value(namespace, 'deploy_hook', value) assert_set_by_user_with_value(namespace, 'renew_hook', value) def test_deploy_hook_sets_renew_hook(self): value = "foo" namespace = self.parse( ["--deploy-hook", value, "--disable-hook-validation"]) assert_set_by_user_with_value(namespace, 'deploy_hook', value) assert_set_by_user_with_value(namespace, 'renew_hook', value) def test_renew_hook_conflict(self): with mock.patch("certbot._internal.cli.sys.stderr"): with pytest.raises(SystemExit): self.parse("--deploy-hook foo --renew-hook bar".split()) def test_renew_hook_matches_deploy_hook(self): value = "foo" namespace = self.parse(["--deploy-hook", value, "--renew-hook", value, "--disable-hook-validation"]) assert_set_by_user_with_value(namespace, 'deploy_hook', value) assert_set_by_user_with_value(namespace, 'renew_hook', value) def test_renew_hook_does_not_set_renew_hook(self): value = "foo" namespace = self.parse( ["--renew-hook", value, "--disable-hook-validation"]) assert namespace.deploy_hook is None assert_set_by_user_with_value(namespace, 'renew_hook', value) def test_max_log_backups_error(self): with mock.patch('certbot._internal.cli.sys.stderr'): with pytest.raises(SystemExit): self.parse("--max-log-backups foo".split()) with pytest.raises(SystemExit): self.parse("--max-log-backups -42".split()) def test_max_log_backups_success(self): value = "42" namespace = self.parse(["--max-log-backups", value]) assert_set_by_user_with_value(namespace, 'max_log_backups', int(value)) def test_unchanging_defaults(self): namespace = self.parse([]) assert_value_and_source(namespace, 'domains', [], ArgumentSource.DEFAULT) assert_value_and_source(namespace, 'pref_challs', [], ArgumentSource.DEFAULT) namespace.pref_challs = [challenges.HTTP01.typ] namespace.domains = ['example.com'] namespace = self.parse([]) assert_value_and_source(namespace, 'domains', [], ArgumentSource.DEFAULT) assert_value_and_source(namespace, 'pref_challs', [], ArgumentSource.DEFAULT) def test_no_directory_hooks_set(self): namespace = self.parse(["--no-directory-hooks"]) assert_set_by_user_with_value(namespace, 'directory_hooks', False) def test_no_directory_hooks_unset(self): namespace = self.parse([]) assert_value_and_source(namespace, 'directory_hooks', True, ArgumentSource.DEFAULT) def test_delete_after_revoke(self): namespace = self.parse(["--delete-after-revoke"]) assert_set_by_user_with_value(namespace, 'delete_after_revoke', True) def test_delete_after_revoke_default(self): namespace = self.parse([]) assert namespace.delete_after_revoke is None assert not namespace.set_by_user('delete_after_revoke') def test_no_delete_after_revoke(self): namespace = self.parse(["--no-delete-after-revoke"]) assert_set_by_user_with_value(namespace, 'delete_after_revoke', False) def test_allow_subset_with_wildcard(self): with pytest.raises(errors.Error): self.parse("--allow-subset-of-names -d *.example.org".split()) def test_route53_no_revert(self): for help_flag in ['-h', '--help']: for topic in ['all', 'plugins', 'dns-route53']: assert 'certbot-route53:auth' not in self._help_output([help_flag, topic]) def test_parse_args_hosts_and_auto_hosts(self): with pytest.raises(errors.Error): self.parse(['--hsts', '--auto-hsts']) def test_parse_with_multiple_argument_sources(self): DEFAULT_VALUE = flag_default('server') CONFIG_FILE_VALUE = 'configfile.biz' COMMAND_LINE_VALUE = 'commandline.edu' # check that the default is set namespace = self.parse(['certonly']) assert_value_and_source(namespace, 'server', DEFAULT_VALUE, ArgumentSource.DEFAULT) with tempfile.NamedTemporaryFile() as tmp_config: tmp_config.close() # close now because of compatibility issues on Windows with open(tmp_config.name, 'w') as file_h: file_h.write(f'server = {CONFIG_FILE_VALUE}') # first, just provide a value from a config file namespace = self.parse([ 'certonly', '-c', tmp_config.name, ]) assert_value_and_source(namespace, 'server', CONFIG_FILE_VALUE, ArgumentSource.CONFIG_FILE) # now provide config file + command line values namespace = self.parse([ 'certonly', '-c', tmp_config.name, '--server', COMMAND_LINE_VALUE, ]) assert_value_and_source(namespace, 'server', COMMAND_LINE_VALUE, ArgumentSource.COMMAND_LINE) def test_abbreviated_arguments(self): # Argparse's "allow_abbrev" option (which is True by default) allows # for unambiguous partial arguments (e.g. "--preferred-chal dns" will be # interepreted the same as "--preferred-challenges dns") namespace = self.parse('--preferred-chal dns --no-dir') assert_set_by_user_with_value(namespace, 'pref_challs', ['dns-01']) assert_set_by_user_with_value(namespace, 'directory_hooks', False) with tempfile.NamedTemporaryFile() as tmp_config: tmp_config.close() # close now because of compatibility issues on Windows with open(tmp_config.name, 'w') as file_h: file_h.write('preferred-chal = dns') namespace = self.parse([ 'certonly', '--config', tmp_config.name, ]) assert_set_by_user_with_value(namespace, 'pref_challs', ['dns-01']) @mock.patch('certbot._internal.hooks.validate_hooks') def test_argument_with_equals(self, unsused_mock_validate_hooks): namespace = self.parse('-d=example.com') assert_set_by_user_with_value(namespace, 'domains', ['example.com']) # make sure it doesn't choke on equals signs being present in the argument value plugins = disco.PluginsRegistry.find_all() namespace = cli.prepare_and_parse_args(plugins, ['run', '--pre-hook="foo=bar"']) assert_set_by_user_with_value(namespace, 'pre_hook', '"foo=bar"') def test_adjacent_short_args(self): namespace = self.parse('-tv') assert_set_by_user_with_value(namespace, 'text_mode', True) assert_set_by_user_with_value(namespace, 'verbose_count', 1) namespace = self.parse('-tvvv') assert_set_by_user_with_value(namespace, 'text_mode', True) assert_set_by_user_with_value(namespace, 'verbose_count', 3) namespace = self.parse('-tvm foo@example.com') assert_set_by_user_with_value(namespace, 'text_mode', True) assert_set_by_user_with_value(namespace, 'verbose_count', 1) assert_set_by_user_with_value(namespace, 'email', 'foo@example.com') def test_arg_with_contained_spaces(self): # This can happen if a user specifies an arg like "-d foo.com" enclosed # in double quotes, or as its own line in a docker-compose.yml file (as # in #9811) namespace = self.parse(['certonly', '-d foo.com']) assert_set_by_user_with_value(namespace, 'domains', ['foo.com']) if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/client_test.py0000664000175100017510000013250014561227515021637 0ustar00ericaerica"""Tests for certbot._internal.client.""" import contextlib import datetime import platform import shutil import sys import tempfile import unittest from unittest import mock from unittest.mock import MagicMock from josepy import interfaces import pytest from certbot import errors from certbot import util from certbot._internal import account from certbot._internal import constants from certbot._internal.display import obj as display_obj from certbot.compat import os import certbot.tests.util as test_util KEY = test_util.load_vector("rsa512_key.pem") CSR_SAN = test_util.load_vector("csr-san_512.pem") # pylint: disable=line-too-long class DetermineUserAgentTest(test_util.ConfigTestCase): """Tests for certbot._internal.client.determine_user_agent.""" def _call(self): from certbot._internal.client import determine_user_agent return determine_user_agent(self.config) @mock.patch.dict(os.environ, {"CERTBOT_DOCS": "1"}) def test_docs_value(self): self._test(expect_doc_values=True) @mock.patch.dict(os.environ, {}) def test_real_values(self): self._test(expect_doc_values=False) def _test(self, expect_doc_values): ua = self._call() if expect_doc_values: doc_value_check = self.assertIn real_value_check = self.assertNotIn else: doc_value_check = self.assertNotIn real_value_check = self.assertIn doc_value_check("OS_NAME OS_VERSION", ua) doc_value_check("major.minor.patchlevel", ua) real_value_check(util.get_os_info_ua(), ua) real_value_check(platform.python_version(), ua) class RegisterTest(test_util.ConfigTestCase): """Tests for certbot._internal.client.register.""" def setUp(self): super().setUp() self.config.rsa_key_size = 1024 self.config.register_unsafely_without_email = False self.config.email = "alias@example.com" self.account_storage = account.AccountMemoryStorage() self.tos_cb = mock.MagicMock() display_obj.set_display(MagicMock()) def _call(self): from certbot._internal.client import register return register(self.config, self.account_storage, self.tos_cb) @staticmethod def _public_key_mock(): m = mock.Mock(__class__=interfaces.JSONDeSerializable) m.to_partial_json.return_value = '{"a": 1}' return m @staticmethod def _new_acct_dir_mock(): return "/acme/new-account" @staticmethod def _true_mock(): return True @staticmethod def _false_mock(): return False @staticmethod @contextlib.contextmanager def _patched_acme_client(): with mock.patch('certbot._internal.client.acme_client') as mock_acme_client: yield mock_acme_client.ClientV2 def test_no_tos(self): with self._patched_acme_client() as mock_client: mock_client.new_account().terms_of_service = "http://tos" mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.prepare_subscription") as mock_prepare: mock_client().new_account.side_effect = errors.Error with pytest.raises(errors.Error): self._call() assert mock_prepare.called is False mock_client().new_account.side_effect = None self._call() assert mock_prepare.called is True @mock.patch('certbot._internal.eff.prepare_subscription') def test_empty_meta(self, unused_mock_prepare): # Test that we can handle an ACME server which does not implement the 'meta' # directory object (for terms-of-service handling). with self._patched_acme_client() as mock_client: from acme.messages import Directory mock_client().directory = Directory.from_json({}) mock_client().external_account_required.side_effect = self._false_mock self._call() assert self.tos_cb.called is False @test_util.patch_display_util() def test_it(self, unused_mock_get_utility): with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription"): self._call() assert self.tos_cb.called is True @mock.patch("certbot._internal.client.display_ops.get_email") def test_email_retry(self, mock_get_email): from acme import messages self.config.noninteractive_mode = False msg = "DNS problem: NXDOMAIN looking up MX for example.com" mx_err = messages.Error.with_code('invalidContact', detail=msg) with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.prepare_subscription") as mock_prepare: mock_client().new_account.side_effect = [mx_err, mock.MagicMock()] self._call() assert mock_get_email.call_count == 1 assert mock_prepare.called is True def test_email_invalid_noninteractive(self): from acme import messages self.config.noninteractive_mode = True msg = "DNS problem: NXDOMAIN looking up MX for example.com" mx_err = messages.Error.with_code('invalidContact', detail=msg) with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription"): mock_client().new_account.side_effect = [mx_err, mock.MagicMock()] with pytest.raises(errors.Error): self._call() def test_needs_email(self): self.config.email = None with pytest.raises(errors.Error): self._call() @mock.patch("certbot._internal.client.logger") def test_without_email(self, mock_logger): with mock.patch("certbot._internal.eff.prepare_subscription") as mock_prepare: with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock self.config.email = None self.config.register_unsafely_without_email = True self.config.dry_run = False self._call() mock_logger.debug.assert_called_once_with(mock.ANY) assert mock_prepare.called is True @mock.patch("certbot._internal.client.display_ops.get_email") def test_dry_run_no_staging_account(self, mock_get_email): """Tests dry-run for no staging account, expect account created with no email""" with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription"): self.config.dry_run = True self._call() # check Certbot did not ask the user to provide an email assert mock_get_email.called is False # check Certbot created an account with no email. Contact should return empty assert not mock_client().new_account.call_args[0][0].contact @test_util.patch_display_util() def test_with_eab_arguments(self, unused_mock_get_utility): with self._patched_acme_client() as mock_client: mock_client().client.directory.__getitem__ = mock.Mock( side_effect=self._new_acct_dir_mock ) mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription"): target = "certbot._internal.client.messages.ExternalAccountBinding.from_data" with mock.patch(target) as mock_eab_from_data: self.config.eab_kid = "test-kid" self.config.eab_hmac_key = "J2OAqW4MHXsrHVa_PVg0Y-L_R4SYw0_aL1le6mfblbE" self._call() assert mock_eab_from_data.called is True @test_util.patch_display_util() def test_without_eab_arguments(self, unused_mock_get_utility): with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription"): target = "certbot._internal.client.messages.ExternalAccountBinding.from_data" with mock.patch(target) as mock_eab_from_data: self.config.eab_kid = None self.config.eab_hmac_key = None self._call() assert mock_eab_from_data.called is False def test_external_account_required_without_eab_arguments(self): with self._patched_acme_client() as mock_client: mock_client().client.net.key.public_key = mock.Mock(side_effect=self._public_key_mock) mock_client().external_account_required.side_effect = self._true_mock with mock.patch("certbot._internal.eff.handle_subscription"): with mock.patch("certbot._internal.client.messages.ExternalAccountBinding.from_data"): self.config.eab_kid = None self.config.eab_hmac_key = None with pytest.raises(errors.Error): self._call() def test_unsupported_error(self): from acme import messages msg = "Test" mx_err = messages.Error.with_code("malformed", detail=msg, title="title") with self._patched_acme_client() as mock_client: mock_client().client.directory.__getitem__ = mock.Mock( side_effect=self._new_acct_dir_mock ) mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription") as mock_handle: mock_client().new_account.side_effect = [mx_err, mock.MagicMock()] with pytest.raises(messages.Error): self._call() assert mock_handle.called is False class ClientTestCommon(test_util.ConfigTestCase): """Common base class for certbot._internal.client.Client tests.""" def setUp(self): super().setUp() self.config.no_verify_ssl = False self.config.allow_subset_of_names = False self.account = mock.MagicMock(**{"key.pem": KEY}) from certbot._internal.client import Client with mock.patch("certbot._internal.client.acme_client") as acme: self.acme_client = acme.ClientV2 self.acme = self.acme_client.return_value = mock.MagicMock() self.client_network = acme.ClientNetwork self.client = Client( config=self.config, account_=self.account, auth=None, installer=None) class ClientTest(ClientTestCommon): """Tests for certbot._internal.client.Client.""" def setUp(self): super().setUp() self.config.allow_subset_of_names = False self.config.dry_run = False self.config.strict_permissions = True self.eg_domains = ["example.com", "www.example.com"] self.eg_order = mock.MagicMock( authorizations=[None], csr_pem=mock.sentinel.csr_pem) def test_init_acme_verify_ssl(self): assert self.client_network.call_args[1]['verify_ssl'] is True def _mock_obtain_certificate(self): self.client.auth_handler = mock.MagicMock() self.client.auth_handler.handle_authorizations.return_value = [None] self.client.auth_handler.deactivate_valid_authorizations.return_value = ([], []) self.acme.finalize_order.return_value = self.eg_order self.acme.new_order.return_value = self.eg_order self.eg_order.update.return_value = self.eg_order def _check_obtain_certificate(self, auth_count=1): if auth_count == 1: self.client.auth_handler.handle_authorizations.assert_called_once_with( self.eg_order, self.config, self.config.allow_subset_of_names) else: assert self.client.auth_handler.handle_authorizations.call_count == auth_count self.acme.finalize_order.assert_called_once_with( self.eg_order, mock.ANY, fetch_alternative_chains=self.config.preferred_chain is not None) @mock.patch("certbot._internal.client.crypto_util") @mock.patch("certbot._internal.client.logger") def test_obtain_certificate_from_csr(self, mock_logger, mock_crypto_util): self._mock_obtain_certificate() test_csr = util.CSR(form="pem", file=None, data=CSR_SAN) auth_handler = self.client.auth_handler self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) orderr = self.acme.new_order(test_csr.data) auth_handler.handle_authorizations(orderr, self.config, False) assert (mock.sentinel.cert, mock.sentinel.chain) == \ self.client.obtain_certificate_from_csr( test_csr, orderr=orderr) mock_crypto_util.find_chain_with_issuer.assert_not_called() # and that the cert was obtained correctly self._check_obtain_certificate() # Test that --preferred-chain results in chain selection self.config.preferred_chain = "some issuer" assert (mock.sentinel.cert, mock.sentinel.chain) == \ self.client.obtain_certificate_from_csr( test_csr, orderr=orderr) mock_crypto_util.find_chain_with_issuer.assert_called_once_with( [orderr.fullchain_pem] + orderr.alternative_fullchains_pem, "some issuer", True) self.config.preferred_chain = None # Test for default issuance_timeout expected_deadline = \ datetime.datetime.now() + datetime.timedelta( seconds=constants.CLI_DEFAULTS["issuance_timeout"]) self.client.obtain_certificate_from_csr(test_csr, orderr=orderr) ((_, deadline), _) = self.client.acme.finalize_order.call_args assert abs(expected_deadline - deadline) <= datetime.timedelta(seconds=1) # Test for specific issuance_timeout (300 seconds) expected_deadline = \ datetime.datetime.now() + datetime.timedelta(seconds=300) self.config.issuance_timeout = 300 self.client.obtain_certificate_from_csr(test_csr, orderr=orderr) ((_, deadline), _) = self.client.acme.finalize_order.call_args assert abs(expected_deadline - deadline) <= datetime.timedelta(seconds=1) # Test for orderr=None assert (mock.sentinel.cert, mock.sentinel.chain) == \ self.client.obtain_certificate_from_csr( test_csr, orderr=None) auth_handler.handle_authorizations.assert_called_with(self.eg_order, self.config, False) # Test for no auth_handler self.client.auth_handler = None with pytest.raises(errors.Error): self.client.obtain_certificate_from_csr(test_csr) mock_logger.error.assert_called_once_with(mock.ANY) @mock.patch("certbot._internal.client.crypto_util") def test_obtain_certificate(self, mock_crypto_util): csr = util.CSR(form="pem", file=None, data=CSR_SAN) mock_crypto_util.generate_csr.return_value = csr mock_crypto_util.generate_key.return_value = mock.sentinel.key self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) self._test_obtain_certificate_common(mock.sentinel.key, csr) mock_crypto_util.generate_key.assert_called_once_with( key_size=self.config.rsa_key_size, key_dir=None, key_type=self.config.key_type, elliptic_curve="secp256r1", strict_permissions=True, ) mock_crypto_util.generate_csr.assert_called_once_with( mock.sentinel.key, self.eg_domains, None, False, True) mock_crypto_util.cert_and_chain_from_fullchain.assert_called_once_with( self.eg_order.fullchain_pem) @mock.patch("certbot._internal.client.crypto_util") def test_obtain_certificate_partial_success(self, mock_crypto_util): csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN) key = util.CSR(form="pem", file=mock.sentinel.key_file, data=CSR_SAN) mock_crypto_util.generate_csr.return_value = csr mock_crypto_util.generate_key.return_value = key self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) authzr = self._authzr_from_domains(["example.com"]) self.config.allow_subset_of_names = True self._test_obtain_certificate_common(key, csr, authzr_ret=authzr, auth_count=2) assert mock_crypto_util.generate_key.call_count == 2 assert mock_crypto_util.generate_csr.call_count == 2 assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 1 @mock.patch("certbot._internal.client.crypto_util") def test_obtain_certificate_finalize_order_partial_success(self, mock_crypto_util): from acme import messages csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN) key = util.CSR(form="pem", file=mock.sentinel.key_file, data=CSR_SAN) mock_crypto_util.generate_csr.return_value = csr mock_crypto_util.generate_key.return_value = key self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) self._mock_obtain_certificate() authzr = self._authzr_from_domains(self.eg_domains) self.eg_order.authorizations = authzr self.client.auth_handler.handle_authorizations.return_value = authzr identifier = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='example.com') subproblem = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier) error_with_subproblems = messages.Error.with_code('malformed', detail='foo', title='title', subproblems=[subproblem]) self.client.acme.finalize_order.side_effect = [error_with_subproblems, mock.DEFAULT] self.config.allow_subset_of_names = True with test_util.patch_display_util(): result = self.client.obtain_certificate(self.eg_domains) assert result == \ (mock.sentinel.cert, mock.sentinel.chain, key, csr) assert self.client.auth_handler.handle_authorizations.call_count == 2 assert self.acme.finalize_order.call_count == 2 successful_domains = [d for d in self.eg_domains if d != 'example.com'] assert mock_crypto_util.generate_key.call_count == 2 mock_crypto_util.generate_csr.assert_has_calls([ mock.call(key, self.eg_domains, None, self.config.must_staple, self.config.strict_permissions), mock.call(key, successful_domains, None, self.config.must_staple, self.config.strict_permissions)]) assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 1 @mock.patch("certbot._internal.client.crypto_util") def test_obtain_certificate_finalize_order_no_retryable_domains(self, mock_crypto_util): from acme import messages csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN) key = util.CSR(form="pem", file=mock.sentinel.key_file, data=CSR_SAN) mock_crypto_util.generate_csr.return_value = csr mock_crypto_util.generate_key.return_value = key self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) self._mock_obtain_certificate() authzr = self._authzr_from_domains(self.eg_domains) self.eg_order.authorizations = authzr self.client.auth_handler.handle_authorizations.return_value = authzr identifier1 = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='example.com') identifier2 = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='www.example.com') subproblem1 = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier1) subproblem2 = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier2) error_with_subproblems = messages.Error.with_code('malformed', detail='foo', title='title', subproblems=[subproblem1, subproblem2]) self.client.acme.finalize_order.side_effect = error_with_subproblems self.config.allow_subset_of_names = True with pytest.raises(messages.Error): self.client.obtain_certificate(self.eg_domains) assert self.client.auth_handler.handle_authorizations.call_count == 1 assert self.acme.finalize_order.call_count == 1 assert mock_crypto_util.generate_key.call_count == 1 assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 0 @mock.patch("certbot._internal.client.crypto_util") def test_obtain_certificate_finalize_order_rejected_identifier_no_subproblems(self, mock_crypto_util): from acme import messages csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN) key = util.CSR(form="pem", file=mock.sentinel.key_file, data=CSR_SAN) mock_crypto_util.generate_csr.return_value = csr mock_crypto_util.generate_key.return_value = key self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) self._mock_obtain_certificate() authzr = self._authzr_from_domains(self.eg_domains) self.eg_order.authorizations = authzr self.client.auth_handler.handle_authorizations.return_value = authzr error = messages.Error.with_code('caa', detail='foo', title='title') self.client.acme.finalize_order.side_effect = error self.config.allow_subset_of_names = True with pytest.raises(messages.Error): self.client.obtain_certificate(self.eg_domains) assert self.client.auth_handler.handle_authorizations.call_count == 1 assert self.acme.finalize_order.call_count == 1 assert mock_crypto_util.generate_key.call_count == 1 assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 0 @mock.patch("certbot._internal.client.crypto_util") def test_obtain_certificate_get_order_partial_success(self, mock_crypto_util): from acme import messages csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN) key = util.CSR(form="pem", file=mock.sentinel.key_file, data=CSR_SAN) mock_crypto_util.generate_csr.return_value = csr mock_crypto_util.generate_key.return_value = key self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) self._mock_obtain_certificate() authzr = self._authzr_from_domains(self.eg_domains) self.eg_order.authorizations = authzr self.client.auth_handler.handle_authorizations.return_value = authzr identifier = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='example.com') subproblem = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier) error_with_subproblems = messages.Error.with_code('malformed', detail='foo', title='title', subproblems=[subproblem]) self.client.acme.new_order.side_effect = [error_with_subproblems, mock.DEFAULT] self.config.allow_subset_of_names = True with test_util.patch_display_util(): result = self.client.obtain_certificate(self.eg_domains) assert result == \ (mock.sentinel.cert, mock.sentinel.chain, key, csr) assert self.client.auth_handler.handle_authorizations.call_count == 1 assert self.acme.new_order.call_count == 2 successful_domains = [d for d in self.eg_domains if d != 'example.com'] assert mock_crypto_util.generate_key.call_count == 2 mock_crypto_util.generate_csr.assert_has_calls([ mock.call(key, self.eg_domains, None, self.config.must_staple, self.config.strict_permissions), mock.call(key, successful_domains, None, self.config.must_staple, self.config.strict_permissions)]) assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 1 @mock.patch("certbot._internal.client.crypto_util") def test_obtain_certificate_get_order_no_retryable_domains(self, mock_crypto_util): from acme import messages csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN) key = util.CSR(form="pem", file=mock.sentinel.key_file, data=CSR_SAN) mock_crypto_util.generate_csr.return_value = csr mock_crypto_util.generate_key.return_value = key self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) self._mock_obtain_certificate() authzr = self._authzr_from_domains(self.eg_domains) self.eg_order.authorizations = authzr self.client.auth_handler.handle_authorizations.return_value = authzr identifier1 = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='example.com') identifier2 = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='www.example.com') subproblem1 = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier1) subproblem2 = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier2) error_with_subproblems = messages.Error.with_code('malformed', detail='foo', title='title', subproblems=[subproblem1, subproblem2]) self.client.acme.new_order.side_effect = error_with_subproblems self.config.allow_subset_of_names = True with pytest.raises(messages.Error): self.client.obtain_certificate(self.eg_domains) assert self.client.auth_handler.handle_authorizations.call_count == 0 assert self.acme.new_order.call_count == 1 assert mock_crypto_util.generate_key.call_count == 1 assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 0 @mock.patch("certbot._internal.client.crypto_util") def test_obtain_certificate_get_order_rejected_identifier_no_subproblems(self, mock_crypto_util): from acme import messages csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN) key = util.CSR(form="pem", file=mock.sentinel.key_file, data=CSR_SAN) mock_crypto_util.generate_csr.return_value = csr mock_crypto_util.generate_key.return_value = key self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) self._mock_obtain_certificate() authzr = self._authzr_from_domains(self.eg_domains) self.eg_order.authorizations = authzr self.client.auth_handler.handle_authorizations.return_value = authzr error = messages.Error.with_code('caa', detail='foo', title='title') self.client.acme.new_order.side_effect = error self.config.allow_subset_of_names = True with pytest.raises(messages.Error): self.client.obtain_certificate(self.eg_domains) assert self.client.auth_handler.handle_authorizations.call_count == 0 assert self.acme.new_order.call_count == 1 assert mock_crypto_util.generate_key.call_count == 1 assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 0 @mock.patch("certbot._internal.client.crypto_util") @mock.patch("certbot._internal.client.acme_crypto_util") def test_obtain_certificate_dry_run(self, mock_acme_crypto, mock_crypto): csr = util.CSR(form="pem", file=None, data=CSR_SAN) mock_acme_crypto.make_csr.return_value = CSR_SAN mock_crypto.make_key.return_value = mock.sentinel.key_pem key = util.Key(file=None, pem=mock.sentinel.key_pem) self._set_mock_from_fullchain(mock_crypto.cert_and_chain_from_fullchain) self.client.config.dry_run = True self._test_obtain_certificate_common(key, csr) mock_crypto.make_key.assert_called_once_with( bits=self.config.rsa_key_size, elliptic_curve="secp256r1", key_type=self.config.key_type, ) mock_acme_crypto.make_csr.assert_called_once_with( mock.sentinel.key_pem, self.eg_domains, self.config.must_staple) mock_crypto.generate_key.assert_not_called() mock_crypto.generate_csr.assert_not_called() assert mock_crypto.cert_and_chain_from_fullchain.call_count == 1 @mock.patch("certbot._internal.client.logger") @mock.patch("certbot._internal.client.crypto_util") @mock.patch("certbot._internal.client.acme_crypto_util") def test_obtain_certificate_dry_run_authz_deactivations_failed(self, mock_acme_crypto, mock_crypto, mock_log): from acme import messages csr = util.CSR(form="pem", file=None, data=CSR_SAN) mock_acme_crypto.make_csr.return_value = CSR_SAN mock_crypto.make_key.return_value = mock.sentinel.key_pem key = util.Key(file=None, pem=mock.sentinel.key_pem) self._set_mock_from_fullchain(mock_crypto.cert_and_chain_from_fullchain) self._mock_obtain_certificate() self.client.config.dry_run = True # Two authzs that are already valid and should get deactivated (dry run) authzrs = self._authzr_from_domains(["example.com", "www.example.com"]) for authzr in authzrs: authzr.body.status = messages.STATUS_VALID # One deactivation succeeds, one fails auth_handler = self.client.auth_handler auth_handler.deactivate_valid_authorizations.return_value = ([authzrs[0]], [authzrs[1]]) # Certificate should get issued despite one failed deactivation self.eg_order.authorizations = authzrs self.client.auth_handler.handle_authorizations.return_value = authzrs with test_util.patch_display_util(): result = self.client.obtain_certificate(self.eg_domains) assert result == (mock.sentinel.cert, mock.sentinel.chain, key, csr) self._check_obtain_certificate(1) # Deactivation success/failure should have been handled properly assert auth_handler.deactivate_valid_authorizations.call_count == 1, \ "Deactivate authorizations should be called" assert self.acme.new_order.call_count == 2, \ "Order should be recreated due to successfully deactivated authorizations" mock_log.warning.assert_called_with("Certbot was unable to obtain fresh authorizations for" " every domain. The dry run will continue, but results" " may not be accurate.") def _set_mock_from_fullchain(self, mock_from_fullchain): mock_cert = mock.Mock() mock_cert.encode.return_value = mock.sentinel.cert mock_chain = mock.Mock() mock_chain.encode.return_value = mock.sentinel.chain mock_from_fullchain.return_value = (mock_cert, mock_chain) def _authzr_from_domains(self, domains): authzr = [] # domain ordering should not be affected by authorization order for domain in reversed(domains): authzr.append( mock.MagicMock( body=mock.MagicMock( identifier=mock.MagicMock( value=domain)))) return authzr def _test_obtain_certificate_common(self, key, csr, authzr_ret=None, auth_count=1): self._mock_obtain_certificate() # return_value is essentially set to (None, None) in # _mock_obtain_certificate(), which breaks this test. # Thus fixed by the next line. authzr = authzr_ret or self._authzr_from_domains(self.eg_domains) self.eg_order.authorizations = authzr self.client.auth_handler.handle_authorizations.return_value = authzr with test_util.patch_display_util(): result = self.client.obtain_certificate(self.eg_domains) assert result == \ (mock.sentinel.cert, mock.sentinel.chain, key, csr) self._check_obtain_certificate(auth_count) @mock.patch('certbot._internal.client.Client.obtain_certificate') @mock.patch('certbot._internal.storage.RenewableCert.new_lineage') def test_obtain_and_enroll_certificate(self, mock_storage, mock_obtain_certificate): domains = ["*.example.com", "example.com"] mock_obtain_certificate.return_value = (mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), None) self.client.config.dry_run = False assert self.client.obtain_and_enroll_certificate(domains, "example_cert") assert self.client.obtain_and_enroll_certificate(domains, None) assert self.client.obtain_and_enroll_certificate(domains[1:], None) self.client.config.dry_run = True assert not self.client.obtain_and_enroll_certificate(domains, None) names = [call[0][0] for call in mock_storage.call_args_list] assert names == ["example_cert", "example.com", "example.com"] @mock.patch("certbot._internal.cli.helpful_parser") def test_save_certificate(self, mock_parser): certs = ["cert_512.pem", "cert-san_512.pem"] tmp_path = tempfile.mkdtemp() cert_pem = test_util.load_vector(certs[0]) chain_pem = (test_util.load_vector(certs[0]) + test_util.load_vector(certs[1])) candidate_cert_path = os.path.join(tmp_path, "certs", "cert_512.pem") candidate_chain_path = os.path.join(tmp_path, "chains", "chain.pem") candidate_fullchain_path = os.path.join(tmp_path, "chains", "fullchain.pem") mock_parser.verb = "certonly" mock_parser.args = ["--cert-path", candidate_cert_path, "--chain-path", candidate_chain_path, "--fullchain-path", candidate_fullchain_path] cert_path, chain_path, fullchain_path = self.client.save_certificate( cert_pem, chain_pem, candidate_cert_path, candidate_chain_path, candidate_fullchain_path) assert os.path.dirname(cert_path) == \ os.path.dirname(candidate_cert_path) assert os.path.dirname(chain_path) == \ os.path.dirname(candidate_chain_path) assert os.path.dirname(fullchain_path) == \ os.path.dirname(candidate_fullchain_path) with open(cert_path, "rb") as cert_file: cert_contents = cert_file.read() assert cert_contents == test_util.load_vector(certs[0]) with open(chain_path, "rb") as chain_file: chain_contents = chain_file.read() assert chain_contents == test_util.load_vector(certs[0]) + \ test_util.load_vector(certs[1]) shutil.rmtree(tmp_path) @test_util.patch_display_util() def test_deploy_certificate_success(self, mock_util): with pytest.raises(errors.Error): self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain", "fullchain") installer = mock.MagicMock() self.client.installer = installer self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain", "fullchain") installer.deploy_cert.assert_called_once_with( cert_path=os.path.abspath("cert"), chain_path=os.path.abspath("chain"), domain='foo.bar', fullchain_path='fullchain', key_path=os.path.abspath("key")) assert installer.save.call_count == 2 installer.restart.assert_called_once_with() @mock.patch('certbot._internal.client.display_util.notify') @test_util.patch_display_util() def test_deploy_certificate_failure(self, mock_util, mock_notify): installer = mock.MagicMock() self.client.installer = installer self.config.installer = "foobar" installer.deploy_cert.side_effect = errors.PluginError with pytest.raises(errors.PluginError): self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain", "fullchain") installer.recovery_routine.assert_called_once_with() mock_notify.assert_any_call('Deploying certificate') @test_util.patch_display_util() def test_deploy_certificate_save_failure(self, mock_util): installer = mock.MagicMock() self.client.installer = installer installer.save.side_effect = errors.PluginError with pytest.raises(errors.PluginError): self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain", "fullchain") installer.recovery_routine.assert_called_once_with() @mock.patch('certbot._internal.client.display_util.notify') @test_util.patch_display_util() def test_deploy_certificate_restart_failure(self, mock_get_utility, mock_notify): installer = mock.MagicMock() installer.restart.side_effect = [errors.PluginError, None] self.client.installer = installer with pytest.raises(errors.PluginError): self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain", "fullchain") mock_notify.assert_called_with( 'We were unable to install your certificate, however, we successfully restored ' 'your server to its prior configuration.') installer.rollback_checkpoints.assert_called_once_with() assert installer.restart.call_count == 2 @mock.patch('certbot._internal.client.logger') @test_util.patch_display_util() def test_deploy_certificate_restart_failure2(self, mock_get_utility, mock_logger): installer = mock.MagicMock() installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError self.client.installer = installer with pytest.raises(errors.PluginError): self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain", "fullchain") assert mock_logger.error.call_count == 1 assert 'An error occurred and we failed to restore your config' in \ mock_logger.error.call_args[0][0] installer.rollback_checkpoints.assert_called_once_with() assert installer.restart.call_count == 1 def test_choose_lineage_name(self): sep = os.path.sep invalid_domains = [f"exam{sep}ple.com"] valid_domains = ["example.com"] invalid_certname = f"foo{sep}.bar" valid_certname = "foo.bar" invalid_wildcard_domain = [f"*.exam{sep}ple.com"] # Verify errors are raised when invalid lineagename is chosen. with pytest.raises(errors.Error): self.client._choose_lineagename(invalid_domains, None) with pytest.raises(errors.Error): self.client._choose_lineagename(invalid_domains, invalid_certname) with pytest.raises(errors.Error): self.client._choose_lineagename(valid_domains, invalid_certname) with pytest.raises(errors.Error): self.client._choose_lineagename(invalid_wildcard_domain, None) # Verify no error is raised when invalid domain is overriden by valid certname. self.client._choose_lineagename(invalid_domains, valid_certname) class EnhanceConfigTest(ClientTestCommon): """Tests for certbot._internal.client.Client.enhance_config.""" def setUp(self): super().setUp() self.config.hsts = False self.config.redirect = False self.config.staple = False self.config.uir = False self.domain = "example.org" def test_no_installer(self): with pytest.raises(errors.Error): self.client.enhance_config([self.domain], None) def test_unsupported(self): self.client.installer = mock.MagicMock() self.client.installer.supported_enhancements.return_value = [] self.config.redirect = None self.config.hsts = True with mock.patch("certbot._internal.client.logger") as mock_logger: self.client.enhance_config([self.domain], None) assert mock_logger.error.call_count == 1 self.client.installer.enhance.assert_not_called() @mock.patch("certbot._internal.client.logger") def test_already_exists_header(self, mock_log): self.config.hsts = True self._test_with_already_existing() assert mock_log.info.called is True assert mock_log.info.call_args[0][1] == \ 'Strict-Transport-Security' @mock.patch("certbot._internal.client.logger") def test_already_exists_redirect(self, mock_log): self.config.redirect = True self._test_with_already_existing() assert mock_log.info.called is True assert mock_log.info.call_args[0][1] == \ 'redirect' @mock.patch("certbot._internal.client.logger") def test_config_set_no_warning_redirect(self, mock_log): self.config.redirect = False self._test_with_already_existing() assert mock_log.warning.called is False @mock.patch("certbot._internal.client.logger") def test_no_warn_redirect(self, mock_log): self.config.redirect = None self._test_with_all_supported() assert mock_log.warning.called is False def test_no_ask_hsts(self): self.config.hsts = True self._test_with_all_supported() self.client.installer.enhance.assert_called_with( self.domain, "ensure-http-header", "Strict-Transport-Security") def test_no_ask_redirect(self): self.config.redirect = True self._test_with_all_supported() self.client.installer.enhance.assert_called_with( self.domain, "redirect", None) def test_no_ask_staple(self): self.config.staple = True self._test_with_all_supported() self.client.installer.enhance.assert_called_with( self.domain, "staple-ocsp", None) def test_no_ask_uir(self): self.config.uir = True self._test_with_all_supported() self.client.installer.enhance.assert_called_with( self.domain, "ensure-http-header", "Upgrade-Insecure-Requests") def test_enhance_failure(self): self.client.installer = mock.MagicMock() self.client.installer.enhance.side_effect = errors.PluginError self._test_error(enhance_error=True) self.client.installer.recovery_routine.assert_called_once_with() def test_save_failure(self): self.client.installer = mock.MagicMock() self.client.installer.save.side_effect = errors.PluginError self._test_error() self.client.installer.recovery_routine.assert_called_once_with() self.client.installer.save.assert_called_once_with(mock.ANY) def test_restart_failure(self): self.client.installer = mock.MagicMock() self.client.installer.restart.side_effect = [errors.PluginError, None] self._test_error_with_rollback() def test_restart_failure2(self): installer = mock.MagicMock() installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError self.client.installer = installer self._test_error_with_rollback() def _test_error_with_rollback(self): self._test_error() assert self.client.installer.restart.called is True def _test_error(self, enhance_error=False, restart_error=False): self.config.redirect = True with mock.patch('certbot._internal.client.logger') as mock_logger, \ test_util.patch_display_util() as mock_gu: with pytest.raises(errors.PluginError): self._test_with_all_supported() if enhance_error: assert mock_logger.error.call_count == 1 assert 'Unable to set the %s enhancement for %s.' == mock_logger.error.call_args_list[0][0][0] if restart_error: mock_logger.critical.assert_called_with( 'Rolling back to previous server configuration...') def _test_with_all_supported(self): if self.client.installer is None: self.client.installer = mock.MagicMock() self.client.installer.supported_enhancements.return_value = [ "ensure-http-header", "redirect", "staple-ocsp"] self.client.enhance_config([self.domain], None) assert self.client.installer.save.call_count == 1 assert self.client.installer.restart.call_count == 1 def _test_with_already_existing(self): self.client.installer = mock.MagicMock() self.client.installer.supported_enhancements.return_value = [ "ensure-http-header", "redirect", "staple-ocsp"] self.client.installer.enhance.side_effect = errors.PluginEnhancementAlreadyPresent() self.client.enhance_config([self.domain], None) class RollbackTest(unittest.TestCase): """Tests for certbot._internal.client.rollback.""" def setUp(self): self.m_install = mock.MagicMock() @classmethod def _call(cls, checkpoints, side_effect): from certbot._internal.client import rollback with mock.patch("certbot._internal.client.plugin_selection.pick_installer") as mpi: mpi.side_effect = side_effect rollback(None, checkpoints, {}, mock.MagicMock()) def test_no_problems(self): self._call(1, self.m_install) assert self.m_install().rollback_checkpoints.call_count == 1 assert self.m_install().restart.call_count == 1 def test_no_installer(self): self._call(1, None) # Just make sure no exceptions are raised if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3310835 certbot-2.9.0/certbot/_internal/tests/compat/0000775000175100017510000000000014561227516020233 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/compat/__init__.py0000664000175100017510000000000014561227515022331 0ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/compat/filesystem_test.py0000664000175100017510000006543014561227515024037 0ustar00ericaerica"""Tests for certbot.compat.filesystem""" import contextlib import errno import sys import unittest from unittest import mock import pytest from certbot import util from certbot._internal import lock from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util from certbot.tests.util import TempDirTestCase try: import ntsecuritycon import win32api import win32security POSIX_MODE = False except ImportError: POSIX_MODE = True EVERYBODY_SID = 'S-1-1-0' SYSTEM_SID = 'S-1-5-18' ADMINS_SID = 'S-1-5-32-544' @unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows security') class WindowsChmodTests(TempDirTestCase): """Unit tests for Windows chmod function in filesystem module""" def setUp(self): super().setUp() self.probe_path = _create_probe(self.tempdir) def test_symlink_resolution(self): link_path = os.path.join(self.tempdir, 'link') os.symlink(self.probe_path, link_path) ref_dacl_probe = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl() ref_dacl_link = _get_security_dacl(link_path).GetSecurityDescriptorDacl() filesystem.chmod(link_path, 0o700) # Assert the real file is impacted, not the link. cur_dacl_probe = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl() cur_dacl_link = _get_security_dacl(link_path).GetSecurityDescriptorDacl() assert not filesystem._compare_dacls(ref_dacl_probe, cur_dacl_probe) # pylint: disable=protected-access assert filesystem._compare_dacls(ref_dacl_link, cur_dacl_link) # pylint: disable=protected-access def test_world_permission(self): everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) filesystem.chmod(self.probe_path, 0o700) dacl = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl() assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == everybody] filesystem.chmod(self.probe_path, 0o704) dacl = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl() assert [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == everybody] def test_group_permissions_noop(self): filesystem.chmod(self.probe_path, 0o700) ref_dacl_probe = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl() filesystem.chmod(self.probe_path, 0o740) cur_dacl_probe = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl() assert filesystem._compare_dacls(ref_dacl_probe, cur_dacl_probe) # pylint: disable=protected-access def test_admin_permissions(self): system = win32security.ConvertStringSidToSid(SYSTEM_SID) admins = win32security.ConvertStringSidToSid(ADMINS_SID) filesystem.chmod(self.probe_path, 0o400) dacl = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl() system_aces = [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == system] admin_aces = [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == admins] assert len(system_aces) == 1 assert len(admin_aces) == 1 assert system_aces[0][1] == ntsecuritycon.FILE_ALL_ACCESS assert admin_aces[0][1] == ntsecuritycon.FILE_ALL_ACCESS def test_read_flag(self): self._test_flag(4, ntsecuritycon.FILE_GENERIC_READ) def test_execute_flag(self): self._test_flag(1, ntsecuritycon.FILE_GENERIC_EXECUTE) def test_write_flag(self): self._test_flag(2, (ntsecuritycon.FILE_ALL_ACCESS ^ ntsecuritycon.FILE_GENERIC_READ ^ ntsecuritycon.FILE_GENERIC_EXECUTE)) def test_full_flag(self): self._test_flag(7, ntsecuritycon.FILE_ALL_ACCESS) def _test_flag(self, everyone_mode, windows_flag): # Note that flag is tested against `everyone`, not `user`, because practically these unit # tests are executed with admin privilege, so current user is effectively the admins group, # and so will always have all rights. filesystem.chmod(self.probe_path, 0o700 | everyone_mode) dacl = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl() everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) acls_everybody = [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == everybody] assert len(acls_everybody) == 1 acls_everybody = acls_everybody[0] assert acls_everybody[1] == windows_flag def test_user_admin_dacl_consistency(self): # Set ownership of target to authenticated user authenticated_user, _, _ = win32security.LookupAccountName("", win32api.GetUserName()) security_owner = _get_security_owner(self.probe_path) _set_owner(self.probe_path, security_owner, authenticated_user) filesystem.chmod(self.probe_path, 0o700) security_dacl = _get_security_dacl(self.probe_path) # We expect three ACE: one for admins, one for system, and one for the user assert security_dacl.GetSecurityDescriptorDacl().GetAceCount() == 3 # Set ownership of target to Administrators user group admin_user = win32security.ConvertStringSidToSid(ADMINS_SID) security_owner = _get_security_owner(self.probe_path) _set_owner(self.probe_path, security_owner, admin_user) filesystem.chmod(self.probe_path, 0o700) security_dacl = _get_security_dacl(self.probe_path) # We expect only two ACE: one for admins, one for system, # since the user is also the admins group assert security_dacl.GetSecurityDescriptorDacl().GetAceCount() == 2 class UmaskTest(TempDirTestCase): def test_umask_on_dir(self): previous_umask = filesystem.umask(0o022) try: dir1 = os.path.join(self.tempdir, 'probe1') filesystem.mkdir(dir1) assert filesystem.check_mode(dir1, 0o755) is True filesystem.umask(0o077) dir2 = os.path.join(self.tempdir, 'dir2') filesystem.mkdir(dir2) assert filesystem.check_mode(dir2, 0o700) is True dir3 = os.path.join(self.tempdir, 'dir3') filesystem.mkdir(dir3, mode=0o777) assert filesystem.check_mode(dir3, 0o700) is True finally: filesystem.umask(previous_umask) def test_umask_on_file(self): previous_umask = filesystem.umask(0o022) try: file1 = os.path.join(self.tempdir, 'probe1') UmaskTest._create_file(file1) assert filesystem.check_mode(file1, 0o755) is True filesystem.umask(0o077) file2 = os.path.join(self.tempdir, 'probe2') UmaskTest._create_file(file2) assert filesystem.check_mode(file2, 0o700) is True file3 = os.path.join(self.tempdir, 'probe3') UmaskTest._create_file(file3) assert filesystem.check_mode(file3, 0o700) is True finally: filesystem.umask(previous_umask) @staticmethod def _create_file(path, mode=0o777): file_desc = None try: file_desc = filesystem.open(path, flags=os.O_CREAT, mode=mode) finally: if file_desc: os.close(file_desc) class ComputePrivateKeyModeTest(TempDirTestCase): def setUp(self): super().setUp() self.probe_path = _create_probe(self.tempdir) def test_compute_private_key_mode(self): filesystem.chmod(self.probe_path, 0o777) new_mode = filesystem.compute_private_key_mode(self.probe_path, 0o600) if POSIX_MODE: # On Linux RWX permissions for group and R permission for world # are persisted from the existing moe assert new_mode == 0o674 else: # On Windows no permission is persisted assert new_mode == 0o600 @unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows security') class WindowsOpenTest(TempDirTestCase): def test_new_file_correct_permissions(self): path = os.path.join(self.tempdir, 'file') desc = filesystem.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0o700) os.close(desc) dacl = _get_security_dacl(path).GetSecurityDescriptorDacl() everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == everybody] def test_existing_file_correct_permissions(self): path = os.path.join(self.tempdir, 'file') open(path, 'w').close() desc = filesystem.open(path, os.O_EXCL | os.O_RDWR, 0o700) os.close(desc) dacl = _get_security_dacl(path).GetSecurityDescriptorDacl() everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == everybody] def test_create_file_on_open(self): # os.O_CREAT | os.O_EXCL + file not exists = OK self._test_one_creation(1, file_exist=False, flags=(os.O_CREAT | os.O_EXCL)) # os.O_CREAT | os.O_EXCL + file exists = EEXIST OS exception with pytest.raises(OSError) as exc_info: self._test_one_creation(2, file_exist=True, flags=(os.O_CREAT | os.O_EXCL)) assert exc_info.value.errno == errno.EEXIST # os.O_CREAT + file not exists = OK self._test_one_creation(3, file_exist=False, flags=os.O_CREAT) # os.O_CREAT + file exists = OK self._test_one_creation(4, file_exist=True, flags=os.O_CREAT) # os.O_CREAT + file exists (locked) = EACCES OS exception path = os.path.join(self.tempdir, '5') open(path, 'w').close() filelock = lock.LockFile(path) try: with pytest.raises(OSError) as exc_info: self._test_one_creation(5, file_exist=True, flags=os.O_CREAT) assert exc_info.value.errno == errno.EACCES finally: filelock.release() # os.O_CREAT not set + file not exists = OS exception with pytest.raises(OSError): self._test_one_creation(6, file_exist=False, flags=os.O_RDONLY) def _test_one_creation(self, num, file_exist, flags): one_file = os.path.join(self.tempdir, str(num)) if file_exist and not os.path.exists(one_file): with open(one_file, 'w'): pass handler = None try: handler = filesystem.open(one_file, flags) finally: if handler: os.close(handler) class TempUmaskTests(test_util.TempDirTestCase): """Tests for using the TempUmask class in `with` statements""" def _check_umask(self): old_umask = filesystem.umask(0) filesystem.umask(old_umask) return old_umask def test_works_normally(self): filesystem.umask(0o0022) assert self._check_umask() == 0o0022 with filesystem.temp_umask(0o0077): assert self._check_umask() == 0o0077 assert self._check_umask() == 0o0022 def test_resets_umask_after_exception(self): filesystem.umask(0o0022) assert self._check_umask() == 0o0022 try: with filesystem.temp_umask(0o0077): assert self._check_umask() == 0o0077 raise Exception() except: assert self._check_umask() == 0o0022 @unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security') class WindowsMkdirTests(test_util.TempDirTestCase): """Unit tests for Windows mkdir + makedirs functions in filesystem module""" def test_mkdir_correct_permissions(self): path = os.path.join(self.tempdir, 'dir') filesystem.mkdir(path, 0o700) everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) dacl = _get_security_dacl(path).GetSecurityDescriptorDacl() assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == everybody] def test_makedirs_correct_permissions(self): path = os.path.join(self.tempdir, 'dir') subpath = os.path.join(path, 'subpath') filesystem.makedirs(subpath, 0o700) everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) dacl = _get_security_dacl(subpath).GetSecurityDescriptorDacl() assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == everybody] def test_makedirs_switch_os_mkdir(self): path = os.path.join(self.tempdir, 'dir') import os as std_os # pylint: disable=os-module-forbidden original_mkdir = std_os.mkdir filesystem.makedirs(path) assert original_mkdir == std_os.mkdir try: filesystem.makedirs(path) # Will fail because path already exists except OSError: pass assert original_mkdir == std_os.mkdir class MakedirsTests(test_util.TempDirTestCase): """Unit tests for makedirs function in filesystem module""" def test_makedirs_correct_permissions(self): path = os.path.join(self.tempdir, 'dir') subpath = os.path.join(path, 'subpath') previous_umask = filesystem.umask(0o022) try: filesystem.makedirs(subpath, 0o700) assert filesystem.check_mode(path, 0o700) assert filesystem.check_mode(subpath, 0o700) finally: filesystem.umask(previous_umask) class CopyOwnershipAndModeTest(test_util.TempDirTestCase): """Tests about copy_ownership_and_apply_mode, copy_ownership_and_mode and has_same_ownership""" def setUp(self): super().setUp() self.probe_path = _create_probe(self.tempdir) @unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security') def test_copy_ownership_and_apply_mode_windows(self): system = win32security.ConvertStringSidToSid(SYSTEM_SID) security = win32security.SECURITY_ATTRIBUTES().SECURITY_DESCRIPTOR security.SetSecurityDescriptorOwner(system, False) with mock.patch('win32security.GetFileSecurity') as mock_get: with mock.patch('win32security.SetFileSecurity') as mock_set: mock_get.return_value = security filesystem.copy_ownership_and_apply_mode( 'dummy', self.probe_path, 0o700, copy_user=True, copy_group=False) assert mock_set.call_count == 2 first_call = mock_set.call_args_list[0] security = first_call[0][2] assert system == security.GetSecurityDescriptorOwner() second_call = mock_set.call_args_list[1] security = second_call[0][2] dacl = security.GetSecurityDescriptorDacl() everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) assert dacl.GetAceCount() assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount()) if dacl.GetAce(index)[2] == everybody] @unittest.skipUnless(POSIX_MODE, reason='Test specific to Linux security') def test_copy_ownership_and_apply_mode_linux(self): with mock.patch('os.chown') as mock_chown: with mock.patch('os.chmod') as mock_chmod: with mock.patch('os.stat') as mock_stat: mock_stat.return_value.st_uid = 50 mock_stat.return_value.st_gid = 51 filesystem.copy_ownership_and_apply_mode( 'dummy', self.probe_path, 0o700, copy_user=True, copy_group=True) mock_chown.assert_called_once_with(self.probe_path, 50, 51) mock_chmod.assert_called_once_with(self.probe_path, 0o700) def test_has_same_ownership(self): path1 = os.path.join(self.tempdir, 'test1') path2 = os.path.join(self.tempdir, 'test2') util.safe_open(path1, 'w').close() util.safe_open(path2, 'w').close() assert filesystem.has_same_ownership(path1, path2) is True @unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security') def test_copy_ownership_and_mode_windows(self): src = self.probe_path dst = _create_probe(self.tempdir, name='dst') filesystem.chmod(src, 0o700) assert filesystem.check_mode(src, 0o700) is True assert filesystem.check_mode(dst, 0o744) is True # Checking an actual change of owner is tricky during a unit test, since we do not know # if any user exists beside the current one. So we mock _copy_win_ownership. It's behavior # have been checked theoretically with test_copy_ownership_and_apply_mode_windows. with mock.patch('certbot.compat.filesystem._copy_win_ownership') as mock_copy_owner: filesystem.copy_ownership_and_mode(src, dst) mock_copy_owner.assert_called_once_with(src, dst) assert filesystem.check_mode(dst, 0o700) is True class CheckPermissionsTest(test_util.TempDirTestCase): """Tests relative to functions that check modes.""" def setUp(self): super().setUp() self.probe_path = _create_probe(self.tempdir) def test_check_mode(self): assert filesystem.check_mode(self.probe_path, 0o744) is True filesystem.chmod(self.probe_path, 0o700) assert not filesystem.check_mode(self.probe_path, 0o744) @unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security') def test_check_owner_windows(self): assert filesystem.check_owner(self.probe_path) is True system = win32security.ConvertStringSidToSid(SYSTEM_SID) security = win32security.SECURITY_ATTRIBUTES().SECURITY_DESCRIPTOR security.SetSecurityDescriptorOwner(system, False) with mock.patch('win32security.GetFileSecurity') as mock_get: mock_get.return_value = security assert not filesystem.check_owner(self.probe_path) @unittest.skipUnless(POSIX_MODE, reason='Test specific to Linux security') def test_check_owner_linux(self): assert filesystem.check_owner(self.probe_path) is True import os as std_os # pylint: disable=os-module-forbidden # See related inline comment in certbot.compat.filesystem.check_owner method # that explains why MyPy/PyLint check disable is needed here. uid = std_os.getuid() with mock.patch('os.getuid') as mock_uid: mock_uid.return_value = uid + 1 assert not filesystem.check_owner(self.probe_path) def test_check_permissions(self): assert filesystem.check_permissions(self.probe_path, 0o744) is True with mock.patch('certbot.compat.filesystem.check_mode') as mock_mode: mock_mode.return_value = False assert not filesystem.check_permissions(self.probe_path, 0o744) with mock.patch('certbot.compat.filesystem.check_owner') as mock_owner: mock_owner.return_value = False assert not filesystem.check_permissions(self.probe_path, 0o744) def test_check_min_permissions(self): filesystem.chmod(self.probe_path, 0o744) assert filesystem.has_min_permissions(self.probe_path, 0o744) is True filesystem.chmod(self.probe_path, 0o700) assert not filesystem.has_min_permissions(self.probe_path, 0o744) filesystem.chmod(self.probe_path, 0o741) assert not filesystem.has_min_permissions(self.probe_path, 0o744) def test_is_world_reachable(self): filesystem.chmod(self.probe_path, 0o744) assert filesystem.has_world_permissions(self.probe_path) is True filesystem.chmod(self.probe_path, 0o700) assert not filesystem.has_world_permissions(self.probe_path) class OsReplaceTest(test_util.TempDirTestCase): """Test to ensure consistent behavior of rename method""" def test_os_replace_to_existing_file(self): """Ensure that replace will effectively rename src into dst for all platforms.""" src = os.path.join(self.tempdir, 'src') dst = os.path.join(self.tempdir, 'dst') open(src, 'w').close() open(dst, 'w').close() # On Windows, a direct call to os.rename would fail because dst already exists. filesystem.replace(src, dst) assert not os.path.exists(src) assert os.path.exists(dst) is True class RealpathTest(test_util.TempDirTestCase): """Tests for realpath method""" def setUp(self): super().setUp() self.probe_path = _create_probe(self.tempdir) def test_symlink_resolution(self): # Remove any symlinks already in probe_path self.probe_path = filesystem.realpath(self.probe_path) # Absolute resolution link_path = os.path.join(self.tempdir, 'link_abs') os.symlink(self.probe_path, link_path) assert self.probe_path == filesystem.realpath(self.probe_path) assert self.probe_path == filesystem.realpath(link_path) # Relative resolution curdir = os.getcwd() link_path = os.path.join(self.tempdir, 'link_rel') probe_name = os.path.basename(self.probe_path) try: os.chdir(os.path.dirname(self.probe_path)) os.symlink(probe_name, link_path) assert self.probe_path == filesystem.realpath(probe_name) assert self.probe_path == filesystem.realpath(link_path) finally: os.chdir(curdir) def test_symlink_loop_mitigation(self): link1_path = os.path.join(self.tempdir, 'link1') link2_path = os.path.join(self.tempdir, 'link2') link3_path = os.path.join(self.tempdir, 'link3') os.symlink(link1_path, link2_path) os.symlink(link2_path, link3_path) os.symlink(link3_path, link1_path) with pytest.raises(RuntimeError, match='link1 is a loop!') as error: filesystem.realpath(link1_path) class IsExecutableTest(test_util.TempDirTestCase): """Tests for is_executable method""" def test_not_executable(self): file_path = os.path.join(self.tempdir, "foo") # On Windows a file created within Certbot will always have all permissions to the # Administrators group set. Since the unit tests are typically executed under elevated # privileges, it means that current user will always have effective execute rights on the # hook script, and so the test will fail. To prevent that and represent a file created # outside Certbot as typically a hook file is, we mock the _generate_dacl function in # certbot.compat.filesystem to give rights only to the current user. This implies removing # all ACEs except the first one from the DACL created by original _generate_dacl function. from certbot.compat.filesystem import _generate_dacl def _execute_mock(user_sid, mode, mask=None): dacl = _generate_dacl(user_sid, mode, mask) for _ in range(1, dacl.GetAceCount()): dacl.DeleteAce(1) # DeleteAce dynamically updates the internal index mapping. return dacl # create a non-executable file with mock.patch("certbot.compat.filesystem._generate_dacl", side_effect=_execute_mock): os.close(filesystem.open(file_path, os.O_CREAT | os.O_WRONLY, 0o666)) assert not filesystem.is_executable(file_path) @mock.patch("certbot.compat.filesystem.os.path.isfile") @mock.patch("certbot.compat.filesystem.os.access") def test_full_path(self, mock_access, mock_isfile): with _fix_windows_runtime(): mock_access.return_value = True mock_isfile.return_value = True assert filesystem.is_executable("/path/to/exe") is True @mock.patch("certbot.compat.filesystem.os.path.isfile") @mock.patch("certbot.compat.filesystem.os.access") def test_rel_path(self, mock_access, mock_isfile): with _fix_windows_runtime(): mock_access.return_value = True mock_isfile.return_value = True assert filesystem.is_executable("exe") is True @mock.patch("certbot.compat.filesystem.os.path.isfile") @mock.patch("certbot.compat.filesystem.os.access") def test_not_found(self, mock_access, mock_isfile): with _fix_windows_runtime(): mock_access.return_value = True mock_isfile.return_value = False assert not filesystem.is_executable("exe") class ReadlinkTest(unittest.TestCase): @unittest.skipUnless(POSIX_MODE, reason='Tests specific to Linux') @mock.patch("certbot.compat.filesystem.os.readlink") def test_path_posix(self, mock_readlink): mock_readlink.return_value = "/normal/path" assert filesystem.readlink("dummy") == "/normal/path" @unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows') @mock.patch("certbot.compat.filesystem.os.readlink") def test_normal_path_windows(self, mock_readlink): # Python <3.8 mock_readlink.return_value = "C:\\short\\path" assert filesystem.readlink("dummy") == "C:\\short\\path" # Python >=3.8 (os.readlink always returns the extended form) mock_readlink.return_value = "\\\\?\\C:\\short\\path" assert filesystem.readlink("dummy") == "C:\\short\\path" @unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows') @mock.patch("certbot.compat.filesystem.os.readlink") def test_extended_path_windows(self, mock_readlink): # Following path is largely over the 260 characters permitted in the normal form. mock_readlink.return_value = "\\\\?\\C:\\long" + 1000 * "\\path" with pytest.raises(ValueError): filesystem.readlink("dummy") @contextlib.contextmanager def _fix_windows_runtime(): if os.name != 'nt': yield else: with mock.patch('win32security.GetFileSecurity') as mock_get: dacl_mock = mock_get.return_value.GetSecurityDescriptorDacl mode_mock = dacl_mock.return_value.GetEffectiveRightsFromAcl mode_mock.return_value = ntsecuritycon.FILE_GENERIC_EXECUTE yield def _get_security_dacl(target): return win32security.GetFileSecurity(target, win32security.DACL_SECURITY_INFORMATION) def _get_security_owner(target): return win32security.GetFileSecurity(target, win32security.OWNER_SECURITY_INFORMATION) def _set_owner(target, security_owner, user): security_owner.SetSecurityDescriptorOwner(user, False) win32security.SetFileSecurity( target, win32security.OWNER_SECURITY_INFORMATION, security_owner) def _create_probe(tempdir, name='probe'): filesystem.chmod(tempdir, 0o744) probe_path = os.path.join(tempdir, name) util.safe_open(probe_path, 'w', chmod=0o744).close() return probe_path if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/compat/misc_test.py0000664000175100017510000000332314561227515022577 0ustar00ericaerica"""Tests for certbot.compat.misc""" import sys from unittest import mock import pytest from certbot.compat import os class ExecuteStatusTest: """Tests for certbot.compat.misc.execute_command_status.""" @classmethod def _call(cls, *args, **kwargs): from certbot.compat.misc import execute_command_status return execute_command_status(*args, **kwargs) def _test_common(self, returncode, stdout, stderr): given_command = "foo" given_name = "foo-hook" with mock.patch("certbot.compat.misc.subprocess.run") as mock_run: mock_run.return_value.stdout = stdout mock_run.return_value.stderr = stderr mock_run.return_value.returncode = returncode with mock.patch("certbot.compat.misc.logger") as mock_logger: assert self._call(given_name, given_command) == (returncode, stderr, stdout) executed_command = mock_run.call_args[1].get( "args", mock_run.call_args[0][0]) if os.name == 'nt': expected_command = ['powershell.exe', '-Command', given_command] else: expected_command = given_command assert executed_command == expected_command assert executed_command == expected_command mock_logger.info.assert_any_call("Running %s command: %s", given_name, given_command) def test_it(self): for returncode in range(0, 2): for stdout in ("", "Hello World!",): for stderr in ("", "Goodbye Cruel World!"): self._test_common(returncode, stdout, stderr) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/compat/os_test.py0000664000175100017510000000113414561227515022263 0ustar00ericaerica"""Unit test for os module.""" import sys import pytest from certbot.compat import os def test_forbidden_methods(): # Checks for os module for method in ['chmod', 'chown', 'open', 'mkdir', 'makedirs', 'rename', 'replace', 'access', 'stat', 'fstat']: with pytest.raises(RuntimeError): getattr(os, method)() # Checks for os.path module for method in ['realpath']: with pytest.raises(RuntimeError): getattr(os.path, method)() if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/configuration_test.py0000664000175100017510000001726614561227515023243 0ustar00ericaerica"""Tests for certbot.configuration.""" import sys import unittest from unittest import mock import warnings import pytest from certbot import errors from certbot._internal import cli from certbot._internal import constants from certbot._internal.plugins import disco from certbot.compat import misc from certbot.compat import os from certbot.tests import util as test_util class NamespaceConfigTest(test_util.ConfigTestCase): """Tests for certbot.configuration.NamespaceConfig.""" def setUp(self): super().setUp() self.config.foo = 'bar' # pylint: disable=blacklisted-name self.config.server = 'https://acme-server.org:443/new' self.config.https_port = 1234 self.config.http01_port = 4321 def test_init_same_ports(self): self.config.https_port = 4321 from certbot.configuration import NamespaceConfig with pytest.raises(errors.Error): NamespaceConfig(self.config.namespace) def test_proxy_getattr(self): assert self.config.foo == 'bar' assert self.config.work_dir == os.path.join(self.tempdir, 'work') def test_server_path(self): assert ['acme-server.org:443', 'new'] == \ self.config.server_path.split(os.path.sep) self.config.server = ('http://user:pass@acme.server:443' '/p/a/t/h;parameters?query#fragment') assert ['user:pass@acme.server:443', 'p', 'a', 't', 'h'] == \ self.config.server_path.split(os.path.sep) @mock.patch('certbot.configuration.constants') def test_dynamic_dirs(self, mock_constants): mock_constants.ACCOUNTS_DIR = 'acc' mock_constants.BACKUP_DIR = 'backups' mock_constants.CSR_DIR = 'csr' mock_constants.IN_PROGRESS_DIR = '../p' mock_constants.KEY_DIR = 'keys' mock_constants.TEMP_CHECKPOINT_DIR = 't' ref_path = misc.underscores_for_unsupported_characters_in_path( 'acc/acme-server.org:443/new') assert os.path.normpath(self.config.accounts_dir) == \ os.path.normpath(os.path.join(self.config.config_dir, ref_path)) assert os.path.normpath(self.config.backup_dir) == \ os.path.normpath(os.path.join(self.config.work_dir, 'backups')) with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) assert os.path.normpath(self.config.csr_dir) == \ os.path.normpath(os.path.join(self.config.config_dir, 'csr')) assert os.path.normpath(self.config.key_dir) == \ os.path.normpath(os.path.join(self.config.config_dir, 'keys')) assert os.path.normpath(self.config.in_progress_dir) == \ os.path.normpath(os.path.join(self.config.work_dir, '../p')) assert os.path.normpath(self.config.temp_checkpoint_dir) == \ os.path.normpath(os.path.join(self.config.work_dir, 't')) def test_absolute_paths(self): from certbot.configuration import NamespaceConfig config_base = "foo" work_base = "bar" logs_base = "baz" server = "mock.server" mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir', 'logs_dir', 'http01_port', 'https_port', 'domains', 'server']) mock_namespace.config_dir = config_base mock_namespace.work_dir = work_base mock_namespace.logs_dir = logs_base mock_namespace.server = server config = NamespaceConfig(mock_namespace) assert os.path.isabs(config.config_dir) assert config.config_dir == \ os.path.join(os.getcwd(), config_base) assert os.path.isabs(config.work_dir) assert config.work_dir == \ os.path.join(os.getcwd(), work_base) assert os.path.isabs(config.logs_dir) assert config.logs_dir == \ os.path.join(os.getcwd(), logs_base) assert os.path.isabs(config.accounts_dir) assert os.path.isabs(config.backup_dir) with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) assert os.path.isabs(config.csr_dir) assert os.path.isabs(config.key_dir) assert os.path.isabs(config.in_progress_dir) assert os.path.isabs(config.temp_checkpoint_dir) @mock.patch('certbot.configuration.constants') def test_renewal_dynamic_dirs(self, mock_constants): mock_constants.ARCHIVE_DIR = 'a' mock_constants.LIVE_DIR = 'l' mock_constants.RENEWAL_CONFIGS_DIR = 'renewal_configs' assert self.config.default_archive_dir == os.path.join(self.config.config_dir, 'a') assert self.config.live_dir == os.path.join(self.config.config_dir, 'l') assert self.config.renewal_configs_dir == os.path.join( self.config.config_dir, 'renewal_configs') def test_renewal_absolute_paths(self): from certbot.configuration import NamespaceConfig config_base = "foo" work_base = "bar" logs_base = "baz" mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir', 'logs_dir', 'http01_port', 'https_port', 'domains', 'server']) mock_namespace.config_dir = config_base mock_namespace.work_dir = work_base mock_namespace.logs_dir = logs_base config = NamespaceConfig(mock_namespace) assert os.path.isabs(config.default_archive_dir) assert os.path.isabs(config.live_dir) assert os.path.isabs(config.renewal_configs_dir) def test_get_and_set_attr(self): self.config.foo = 42 assert self.config.namespace.foo == 42 self.config.namespace.bar = 1337 assert self.config.bar == 1337 def test_hook_directories(self): assert self.config.renewal_hooks_dir == \ os.path.join(self.config.config_dir, constants.RENEWAL_HOOKS_DIR) assert self.config.renewal_pre_hooks_dir == \ os.path.join(self.config.renewal_hooks_dir, constants.RENEWAL_PRE_HOOKS_DIR) assert self.config.renewal_deploy_hooks_dir == \ os.path.join(self.config.renewal_hooks_dir, constants.RENEWAL_DEPLOY_HOOKS_DIR) assert self.config.renewal_post_hooks_dir == \ os.path.join(self.config.renewal_hooks_dir, constants.RENEWAL_POST_HOOKS_DIR) def test_set_by_user_runtime_overrides(self): assert not self.config.set_by_user('something') self.config.something = 'a value' assert self.config.set_by_user('something') def test_set_by_user_exception(self): from certbot.configuration import NamespaceConfig # a newly created NamespaceConfig has no argument sources dict, so an # exception is raised config = NamespaceConfig(self.config.namespace) with pytest.raises(RuntimeError): config.set_by_user('whatever') # now set an argument sources dict config.set_argument_sources({}) assert not config.set_by_user('whatever') def test_set_by_user_mutables(self): assert not self.config.set_by_user('domains') self.config.domains.append('example.org') assert self.config.set_by_user('domains') if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/crypto_util_test.py0000664000175100017510000004476614561227515022756 0ustar00ericaerica"""Tests for certbot.crypto_util.""" import logging import re import sys import unittest from unittest import mock import OpenSSL import pytest from certbot import errors from certbot import util from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util RSA256_KEY = test_util.load_vector('rsa256_key.pem') RSA256_KEY_PATH = test_util.vector_path('rsa256_key.pem') RSA512_KEY = test_util.load_vector('rsa512_key.pem') RSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem') CERT_PATH = test_util.vector_path('cert_512.pem') CERT = test_util.load_vector('cert_512.pem') SS_CERT_PATH = test_util.vector_path('cert_2048.pem') SS_CERT = test_util.load_vector('cert_2048.pem') P256_KEY = test_util.load_vector('nistp256_key.pem') P256_CERT_PATH = test_util.vector_path('cert-nosans_nistp256.pem') P256_CERT = test_util.load_vector('cert-nosans_nistp256.pem') # CERT_LEAF is signed by CERT_ISSUER. CERT_ALT_ISSUER is a cross-sign of CERT_ISSUER. CERT_LEAF = test_util.load_vector('cert_leaf.pem') CERT_ISSUER = test_util.load_vector('cert_intermediate_1.pem') CERT_ALT_ISSUER = test_util.load_vector('cert_intermediate_2.pem') class GenerateKeyTest(test_util.TempDirTestCase): """Tests for certbot.crypto_util.generate_key.""" def setUp(self): super().setUp() self.workdir = os.path.join(self.tempdir, 'workdir') filesystem.mkdir(self.workdir, mode=0o700) logging.disable(logging.CRITICAL) def tearDown(self): super().tearDown() logging.disable(logging.NOTSET) @classmethod def _call(cls, key_size, key_dir): from certbot.crypto_util import generate_key return generate_key(key_size, key_dir, 'key-certbot.pem', strict_permissions=True) @mock.patch('certbot.crypto_util.make_key') def test_success(self, mock_make): mock_make.return_value = b'key_pem' key = self._call(1024, self.workdir) assert key.pem == b'key_pem' assert 'key-certbot.pem' in key.file assert os.path.exists(os.path.join(self.workdir, key.file)) @mock.patch('certbot.crypto_util.make_key') def test_key_failure(self, mock_make): mock_make.side_effect = ValueError with pytest.raises(ValueError): self._call(431, self.workdir) class GenerateCSRTest(test_util.TempDirTestCase): """Tests for certbot.crypto_util.generate_csr.""" @mock.patch('acme.crypto_util.make_csr') @mock.patch('certbot.crypto_util.util.make_or_verify_dir') def test_it(self, unused_mock_verify, mock_csr): from certbot.crypto_util import generate_csr mock_csr.return_value = b'csr_pem' csr = generate_csr( mock.Mock(pem='dummy_key'), 'example.com', self.tempdir, strict_permissions=True) assert csr.data == b'csr_pem' assert 'csr-certbot.pem' in csr.file class ValidCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.valid_csr.""" @classmethod def _call(cls, csr): from certbot.crypto_util import valid_csr return valid_csr(csr) def test_valid_pem_true(self): assert self._call(test_util.load_vector('csr_512.pem')) def test_valid_pem_san_true(self): assert self._call(test_util.load_vector('csr-san_512.pem')) def test_valid_der_false(self): assert not self._call(test_util.load_vector('csr_512.der')) def test_empty_false(self): assert not self._call('') def test_random_false(self): assert not self._call('foo bar') class CSRMatchesPubkeyTest(unittest.TestCase): """Tests for certbot.crypto_util.csr_matches_pubkey.""" @classmethod def _call(cls, *args, **kwargs): from certbot.crypto_util import csr_matches_pubkey return csr_matches_pubkey(*args, **kwargs) def test_valid_true(self): assert self._call( test_util.load_vector('csr_512.pem'), RSA512_KEY) def test_invalid_false(self): assert not self._call( test_util.load_vector('csr_512.pem'), RSA256_KEY) class ImportCSRFileTest(unittest.TestCase): """Tests for certbot.certbot_util.import_csr_file.""" @classmethod def _call(cls, *args, **kwargs): from certbot.crypto_util import import_csr_file return import_csr_file(*args, **kwargs) def test_der_csr(self): csrfile = test_util.vector_path('csr_512.der') data = test_util.load_vector('csr_512.der') data_pem = test_util.load_vector('csr_512.pem') assert (OpenSSL.crypto.FILETYPE_PEM, util.CSR(file=csrfile, data=data_pem, form="pem"), ["Example.com"]) == \ self._call(csrfile, data) def test_pem_csr(self): csrfile = test_util.vector_path('csr_512.pem') data = test_util.load_vector('csr_512.pem') assert (OpenSSL.crypto.FILETYPE_PEM, util.CSR(file=csrfile, data=data, form="pem"), ["Example.com"],) == \ self._call(csrfile, data) def test_bad_csr(self): with pytest.raises(errors.Error): self._call(test_util.vector_path('cert_512.pem'), test_util.load_vector('cert_512.pem')) class MakeKeyTest(unittest.TestCase): """Tests for certbot.crypto_util.make_key.""" def test_rsa(self): # pylint: disable=no-self-use # RSA Key Type Test from certbot.crypto_util import make_key # Do not test larger keys as it takes too long. OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, make_key(2048)) def test_ec(self): # pylint: disable=no-self-use # ECDSA Key Type Tests from certbot.crypto_util import make_key for (name, bits) in [('secp256r1', 256), ('secp384r1', 384), ('secp521r1', 521)]: pkey = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, make_key(elliptic_curve=name, key_type='ecdsa') ) assert pkey.bits() == bits def test_bad_key_sizes(self): from certbot.crypto_util import make_key # Try a bad key size for RSA and ECDSA with pytest.raises(errors.Error, match='Unsupported RSA key length: 1024'): make_key(bits=1024, key_type='rsa') def test_bad_elliptic_curve_name(self): from certbot.crypto_util import make_key with pytest.raises(errors.Error, match='Unsupported elliptic curve: nothere'): make_key(elliptic_curve="nothere", key_type='ecdsa') def test_bad_key_type(self): from certbot.crypto_util import make_key # Try a bad --key-type with pytest.raises(errors.Error, match=re.escape('Invalid key_type specified: unf. Use [rsa|ecdsa]')): OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, make_key(2048, key_type='unf')) class VerifyCertSetup(unittest.TestCase): """Refactoring for verification tests.""" def setUp(self): self.renewable_cert = mock.MagicMock() self.renewable_cert.cert_path = SS_CERT_PATH self.renewable_cert.chain_path = SS_CERT_PATH self.renewable_cert.key_path = RSA2048_KEY_PATH self.renewable_cert.fullchain_path = test_util.vector_path('cert_fullchain_2048.pem') self.bad_renewable_cert = mock.MagicMock() self.bad_renewable_cert.chain_path = SS_CERT_PATH self.bad_renewable_cert.cert_path = SS_CERT_PATH self.bad_renewable_cert.fullchain_path = SS_CERT_PATH class VerifyRenewableCertTest(VerifyCertSetup): """Tests for certbot.crypto_util.verify_renewable_cert.""" def _call(self, renewable_cert): from certbot.crypto_util import verify_renewable_cert return verify_renewable_cert(renewable_cert) def test_verify_renewable_cert(self): assert self._call(self.renewable_cert) is None @mock.patch('certbot.crypto_util.verify_renewable_cert_sig', side_effect=errors.Error("")) def test_verify_renewable_cert_failure(self, unused_verify_renewable_cert_sign): with pytest.raises(errors.Error): self._call(self.bad_renewable_cert) class VerifyRenewableCertSigTest(VerifyCertSetup): """Tests for certbot.crypto_util.verify_renewable_cert.""" def _call(self, renewable_cert): from certbot.crypto_util import verify_renewable_cert_sig return verify_renewable_cert_sig(renewable_cert) def test_cert_sig_match(self): assert self._call(self.renewable_cert) is None def test_cert_sig_match_ec(self): renewable_cert = mock.MagicMock() renewable_cert.cert_path = P256_CERT_PATH renewable_cert.chain_path = P256_CERT_PATH renewable_cert.key_path = P256_KEY assert self._call(renewable_cert) is None def test_cert_sig_mismatch(self): self.bad_renewable_cert.cert_path = test_util.vector_path('cert_512_bad.pem') with pytest.raises(errors.Error): self._call(self.bad_renewable_cert) class VerifyFullchainTest(VerifyCertSetup): """Tests for certbot.crypto_util.verify_fullchain.""" def _call(self, renewable_cert): from certbot.crypto_util import verify_fullchain return verify_fullchain(renewable_cert) def test_fullchain_matches(self): assert self._call(self.renewable_cert) is None def test_fullchain_mismatch(self): with pytest.raises(errors.Error): self._call(self.bad_renewable_cert) def test_fullchain_ioerror(self): self.bad_renewable_cert.chain = "dog" with pytest.raises(errors.Error): self._call(self.bad_renewable_cert) class VerifyCertMatchesPrivKeyTest(VerifyCertSetup): """Tests for certbot.crypto_util.verify_cert_matches_priv_key.""" def _call(self, renewable_cert): from certbot.crypto_util import verify_cert_matches_priv_key return verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey) def test_cert_priv_key_match(self): self.renewable_cert.cert = SS_CERT_PATH self.renewable_cert.privkey = RSA2048_KEY_PATH assert self._call(self.renewable_cert) is None def test_cert_priv_key_mismatch(self): self.bad_renewable_cert.privkey = RSA256_KEY_PATH self.bad_renewable_cert.cert = SS_CERT_PATH with pytest.raises(errors.Error): self._call(self.bad_renewable_cert) class ValidPrivkeyTest(unittest.TestCase): """Tests for certbot.crypto_util.valid_privkey.""" @classmethod def _call(cls, privkey): from certbot.crypto_util import valid_privkey return valid_privkey(privkey) def test_valid_true(self): assert self._call(RSA512_KEY) def test_empty_false(self): assert not self._call('') def test_random_false(self): assert not self._call('foo bar') class GetSANsFromCertTest(unittest.TestCase): """Tests for certbot.crypto_util.get_sans_from_cert.""" @classmethod def _call(cls, *args, **kwargs): from certbot.crypto_util import get_sans_from_cert return get_sans_from_cert(*args, **kwargs) def test_single(self): assert [] == self._call(test_util.load_vector('cert_512.pem')) def test_san(self): assert ['example.com', 'www.example.com'] == \ self._call(test_util.load_vector('cert-san_512.pem')) class GetNamesFromCertTest(unittest.TestCase): """Tests for certbot.crypto_util.get_names_from_cert.""" @classmethod def _call(cls, *args, **kwargs): from certbot.crypto_util import get_names_from_cert return get_names_from_cert(*args, **kwargs) def test_single(self): assert ['example.com'] == \ self._call(test_util.load_vector('cert_512.pem')) def test_san(self): assert ['example.com', 'www.example.com'] == \ self._call(test_util.load_vector('cert-san_512.pem')) def test_common_name_sans_order(self): # Tests that the common name comes first # followed by the SANS in alphabetical order assert ['example.com'] + ['{0}.example.com'.format(c) for c in 'abcd'] == \ self._call(test_util.load_vector('cert-5sans_512.pem')) def test_parse_non_cert(self): with pytest.raises(OpenSSL.crypto.Error): self._call("hello there") class GetNamesFromReqTest(unittest.TestCase): """Tests for certbot.crypto_util.get_names_from_req.""" @classmethod def _call(cls, *args, **kwargs): from certbot.crypto_util import get_names_from_req return get_names_from_req(*args, **kwargs) def test_nonames(self): assert [] == \ self._call(test_util.load_vector('csr-nonames_512.pem')) def test_nosans(self): assert ['example.com'] == \ self._call(test_util.load_vector('csr-nosans_512.pem')) def test_sans(self): assert ['example.com', 'example.org', 'example.net', 'example.info', 'subdomain.example.com', 'other.subdomain.example.com'] == \ self._call(test_util.load_vector('csr-6sans_512.pem')) def test_der(self): from OpenSSL.crypto import FILETYPE_ASN1 assert ['Example.com'] == \ self._call(test_util.load_vector('csr_512.der'), typ=FILETYPE_ASN1) class CertLoaderTest(unittest.TestCase): """Tests for certbot.crypto_util.pyopenssl_load_certificate""" def test_load_valid_cert(self): from certbot.crypto_util import pyopenssl_load_certificate cert, file_type = pyopenssl_load_certificate(CERT) assert cert.digest('sha256') == \ OpenSSL.crypto.load_certificate(file_type, CERT).digest('sha256') def test_load_invalid_cert(self): from certbot.crypto_util import pyopenssl_load_certificate bad_cert_data = CERT.replace(b"BEGIN CERTIFICATE", b"ASDFASDFASDF!!!") with pytest.raises(errors.Error): pyopenssl_load_certificate(bad_cert_data) class NotBeforeTest(unittest.TestCase): """Tests for certbot.crypto_util.notBefore""" def test_notBefore(self): from certbot.crypto_util import notBefore assert notBefore(CERT_PATH).isoformat() == \ '2014-12-11T22:34:45+00:00' class NotAfterTest(unittest.TestCase): """Tests for certbot.crypto_util.notAfter""" def test_notAfter(self): from certbot.crypto_util import notAfter assert notAfter(CERT_PATH).isoformat() == \ '2014-12-18T22:34:45+00:00' class Sha256sumTest(unittest.TestCase): """Tests for certbot.crypto_util.notAfter""" def test_sha256sum(self): from certbot.crypto_util import sha256sum assert sha256sum(CERT_PATH) == \ '914ffed8daf9e2c99d90ac95c77d54f32cbd556672facac380f0c063498df84e' class CertAndChainFromFullchainTest(unittest.TestCase): """Tests for certbot.crypto_util.cert_and_chain_from_fullchain""" def _parse_and_reencode_pem(self, cert_pem): from OpenSSL import crypto return crypto.dump_certificate(crypto.FILETYPE_PEM, crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)).decode() def test_cert_and_chain_from_fullchain(self): cert_pem = CERT.decode() chain_pem = cert_pem + SS_CERT.decode() fullchain_pem = cert_pem + chain_pem spacey_fullchain_pem = cert_pem + u'\n' + chain_pem crlf_fullchain_pem = fullchain_pem.replace(u'\n', u'\r\n') # In the ACME v1 code path, the fullchain is constructed by loading cert+chain DERs # and using OpenSSL to dump them, so here we confirm that OpenSSL is producing certs # that will be parseable by cert_and_chain_from_fullchain. acmev1_fullchain_pem = self._parse_and_reencode_pem(cert_pem) + \ self._parse_and_reencode_pem(cert_pem) + self._parse_and_reencode_pem(SS_CERT.decode()) from certbot.crypto_util import cert_and_chain_from_fullchain for fullchain in (fullchain_pem, spacey_fullchain_pem, crlf_fullchain_pem, acmev1_fullchain_pem): cert_out, chain_out = cert_and_chain_from_fullchain(fullchain) assert cert_out == cert_pem assert chain_out == chain_pem with pytest.raises(errors.Error): cert_and_chain_from_fullchain(cert_pem) class FindChainWithIssuerTest(unittest.TestCase): """Tests for certbot.crypto_util.find_chain_with_issuer""" @classmethod def _call(cls, fullchains, issuer_cn, **kwargs): from certbot.crypto_util import find_chain_with_issuer return find_chain_with_issuer(fullchains, issuer_cn, kwargs) def _all_fullchains(self): return [CERT_LEAF.decode() + CERT_ISSUER.decode(), CERT_LEAF.decode() + CERT_ALT_ISSUER.decode()] def test_positive_match(self): """Correctly pick the chain based on the root's CN""" fullchains = self._all_fullchains() matched = self._call(fullchains, "Pebble Root CA 0cc6f0") assert matched == fullchains[1] @mock.patch('certbot.crypto_util.logger.info') def test_intermediate_match(self, mock_info): """Don't pick a chain where only an intermediate matches""" fullchains = self._all_fullchains() # Make the second chain actually only contain "Pebble Root CA 0cc6f0" # as an intermediate, not as the root. This wouldn't be a valid chain # (the CERT_ISSUER cert didn't issue the CERT_ALT_ISSUER cert), but the # function under test here doesn't care about that. fullchains[1] = fullchains[1] + CERT_ISSUER.decode() matched = self._call(fullchains, "Pebble Root CA 0cc6f0") assert matched == fullchains[0] mock_info.assert_not_called() @mock.patch('certbot.crypto_util.logger.info') def test_no_match(self, mock_info): fullchains = self._all_fullchains() matched = self._call(fullchains, "non-existent issuer") assert matched == fullchains[0] mock_info.assert_not_called() @mock.patch('certbot.crypto_util.logger.warning') def test_warning_on_no_match(self, mock_warning): fullchains = self._all_fullchains() matched = self._call(fullchains, "non-existent issuer", warn_on_no_match=True) assert matched == fullchains[0] mock_warning.assert_called_once_with("Certbot has been configured to prefer " "certificate chains with issuer '%s', but no chain from the CA matched " "this issuer. Using the default certificate chain instead.", "non-existent issuer") if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3310835 certbot-2.9.0/certbot/_internal/tests/display/0000775000175100017510000000000014561227516020415 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/display/__init__.py0000664000175100017510000000003414561227515022522 0ustar00ericaerica"""Certbot Display Tests""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/display/completer_test.py0000664000175100017510000000720414561227515024022 0ustar00ericaerica"""Test certbot._internal.display.completer.""" from importlib import reload as reload_module import string import sys from typing import List import unittest from unittest import mock import pytest from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util try: import readline # pylint: disable=import-error except ImportError: import certbot._internal.display.dummy_readline as readline # type: ignore class CompleterTest(test_util.TempDirTestCase): """Test certbot._internal.display.completer.Completer.""" def setUp(self): super().setUp() # directories must end with os.sep for completer to # search inside the directory for possible completions if self.tempdir[-1] != os.sep: self.tempdir += os.sep self.paths: List[str] = [] # create some files and directories in temp_dir for c in string.ascii_lowercase: path = os.path.join(self.tempdir, c) self.paths.append(path) if ord(c) % 2: filesystem.mkdir(path) else: with open(path, 'w'): pass def test_complete(self): from certbot._internal.display import completer my_completer = completer.Completer() num_paths = len(self.paths) for i in range(num_paths): completion = my_completer.complete(self.tempdir, i) assert completion in self.paths self.paths.remove(completion) assert len(self.paths) == 0 completion = my_completer.complete(self.tempdir, num_paths) assert completion is None @unittest.skipIf('readline' not in sys.modules, reason='Not relevant if readline is not available.') def test_import_error(self): original_readline = sys.modules['readline'] sys.modules['readline'] = None self.test_context_manager_with_unmocked_readline() sys.modules['readline'] = original_readline def test_context_manager_with_unmocked_readline(self): from certbot._internal.display import completer reload_module(completer) original_completer = readline.get_completer() original_delims = readline.get_completer_delims() with completer.Completer(): pass assert readline.get_completer() == original_completer assert readline.get_completer_delims() == original_delims @mock.patch('certbot._internal.display.completer.readline', autospec=True) def test_context_manager_libedit(self, mock_readline): mock_readline.__doc__ = 'libedit' self._test_context_manager_with_mock_readline(mock_readline) @mock.patch('certbot._internal.display.completer.readline', autospec=True) def test_context_manager_readline(self, mock_readline): mock_readline.__doc__ = 'GNU readline' self._test_context_manager_with_mock_readline(mock_readline) def _test_context_manager_with_mock_readline(self, mock_readline): from certbot._internal.display import completer mock_readline.parse_and_bind.side_effect = enable_tab_completion with completer.Completer(): pass assert mock_readline.parse_and_bind.called is True def enable_tab_completion(unused_command): """Enables readline tab completion using the system specific syntax.""" libedit = readline.__doc__ is not None and 'libedit' in readline.__doc__ command = 'bind ^I rl_complete' if libedit else 'tab: complete' readline.parse_and_bind(command) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/display/internal_util_test.py0000664000175100017510000001204114561227515024674 0ustar00ericaerica"""Test :mod:`certbot._internal.display.util`.""" import io import socket import sys import tempfile import unittest from unittest import mock import pytest from acme import messages as acme_messages from certbot import errors class WrapLinesTest(unittest.TestCase): def test_wrap_lines(self): from certbot._internal.display.util import wrap_lines msg = ("This is just a weak test{0}" "This function is only meant to be for easy viewing{0}" "Test a really really really really really really really really " "really really really really long line...".format('\n')) text = wrap_lines(msg) assert text.count('\n') == 3 class PlaceParensTest(unittest.TestCase): @classmethod def _call(cls, label): from certbot._internal.display.util import parens_around_char return parens_around_char(label) def test_single_letter(self): assert "(a)" == self._call("a") def test_multiple(self): assert "(L)abel" == self._call("Label") assert "(y)es please" == self._call("yes please") class InputWithTimeoutTest(unittest.TestCase): """Tests for certbot._internal.display.util.input_with_timeout.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.display.util import input_with_timeout return input_with_timeout(*args, **kwargs) def test_eof(self): with tempfile.TemporaryFile("r+") as f: with mock.patch("certbot._internal.display.util.sys.stdin", new=f): with pytest.raises(EOFError): self._call() def test_input(self, prompt=None): expected = "foo bar" stdin = io.StringIO(expected + "\n") with mock.patch("certbot.compat.misc.select.select") as mock_select: mock_select.return_value = ([stdin], [], [],) assert self._call(prompt) == expected @mock.patch("certbot._internal.display.util.sys.stdout") def test_input_with_prompt(self, mock_stdout): prompt = "test prompt: " self.test_input(prompt) mock_stdout.write.assert_called_once_with(prompt) mock_stdout.flush.assert_called_once_with() def test_timeout(self): stdin = socket.socket(socket.AF_INET, socket.SOCK_STREAM) stdin.bind(('', 0)) stdin.listen(1) with mock.patch("certbot._internal.display.util.sys.stdin", stdin): with pytest.raises(errors.Error): self._call(timeout=0.001) stdin.close() class SeparateListInputTest(unittest.TestCase): """Test Module functions.""" def setUp(self): self.exp = ["a", "b", "c", "test"] @classmethod def _call(cls, input_): from certbot._internal.display.util import separate_list_input return separate_list_input(input_) def test_commas(self): assert self._call("a,b,c,test") == self.exp def test_spaces(self): assert self._call("a b c test") == self.exp def test_both(self): assert self._call("a, b, c, test") == self.exp def test_mess(self): actual = [ self._call(" a , b c \t test"), self._call(",a, ,, , b c test "), self._call(",,,,, , a b,,, , c,test"), ] for act in actual: assert act == self.exp class SummarizeDomainListTest(unittest.TestCase): @classmethod def _call(cls, domains): from certbot._internal.display.util import summarize_domain_list return summarize_domain_list(domains) def test_single_domain(self): assert "example.com" == self._call(["example.com"]) def test_two_domains(self): assert "example.com and example.org" == \ self._call(["example.com", "example.org"]) def test_many_domains(self): assert "example.com and 2 more domains" == \ self._call(["example.com", "example.org", "a.example.com"]) def test_empty_domains(self): assert "" == self._call([]) class DescribeACMEErrorTest(unittest.TestCase): @classmethod def _call(cls, typ: str = "urn:ietf:params:acme:error:badCSR", title: str = "Unacceptable CSR", detail: str = "CSR contained unknown extensions"): from certbot._internal.display.util import describe_acme_error return describe_acme_error( acme_messages.Error(typ=typ, title=title, detail=detail)) def test_title_and_detail(self): assert "Unacceptable CSR :: CSR contained unknown extensions" == self._call() def test_detail(self): assert "CSR contained unknown extensions" == self._call(title=None) def test_description(self): assert acme_messages.ERROR_CODES["badCSR"] == self._call(title=None, detail=None) def test_unknown_type(self): assert "urn:ietf:params:acme:error:unknownErrorType" == \ self._call(typ="urn:ietf:params:acme:error:unknownErrorType", title=None, detail=None) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/display/obj_test.py0000664000175100017510000003162114561227515022602 0ustar00ericaerica"""Test :mod:`certbot._internal.display.obj`.""" import sys import unittest from unittest import mock import pytest from certbot import errors from certbot._internal.display import obj as display_obj from certbot.display import util as display_util CHOICES = [("First", "Description1"), ("Second", "Description2")] TAGS = ["tag1", "tag2", "tag3"] class FileOutputDisplayTest(unittest.TestCase): """Test stdout display. Most of this class has to deal with visual output. In order to test how the functions look to a user, uncomment the test_visual function. """ def setUp(self): super().setUp() self.mock_stdout = mock.MagicMock() self.displayer = display_obj.FileDisplay(self.mock_stdout, False) @mock.patch("certbot._internal.display.obj.logger") def test_notification_no_pause(self, mock_logger): self.displayer.notification("message", False) string = self.mock_stdout.write.call_args[0][0] assert "message" in string mock_logger.debug.assert_called_with("Notifying user: %s", "message") def test_notification_pause(self): input_with_timeout = "certbot._internal.display.util.input_with_timeout" with mock.patch(input_with_timeout, return_value="enter"): self.displayer.notification("message", force_interactive=True) assert "message" in self.mock_stdout.write.call_args[0][0] def test_notification_noninteractive(self): self._force_noninteractive(self.displayer.notification, "message") string = self.mock_stdout.write.call_args[0][0] assert "message" in string def test_notification_noninteractive2(self): # The main purpose of this test is to make sure we only call # logger.warning once which _force_noninteractive checks internally self._force_noninteractive(self.displayer.notification, "message") string = self.mock_stdout.write.call_args[0][0] assert "message" in string assert self.displayer.skipped_interaction self._force_noninteractive(self.displayer.notification, "message2") string = self.mock_stdout.write.call_args[0][0] assert "message2" in string def test_notification_decoration(self): from certbot.compat import os self.displayer.notification("message", pause=False, decorate=False) string = self.mock_stdout.write.call_args[0][0] assert string == "message" + os.linesep self.displayer.notification("message2", pause=False) string = self.mock_stdout.write.call_args[0][0] assert "- - - " in string assert "message2" + os.linesep in string @mock.patch("certbot._internal.display.obj." "FileDisplay._get_valid_int_ans") def test_menu(self, mock_ans): mock_ans.return_value = (display_util.OK, 1) ret = self.displayer.menu("message", CHOICES, force_interactive=True) assert ret == (display_util.OK, 0) def test_menu_noninteractive(self): default = 0 result = self._force_noninteractive( self.displayer.menu, "msg", CHOICES, default=default) assert result == (display_util.OK, default) def test_input_cancel(self): input_with_timeout = "certbot._internal.display.util.input_with_timeout" with mock.patch(input_with_timeout, return_value="c"): code, _ = self.displayer.input("message", force_interactive=True) assert code, display_util.CANCEL def test_input_normal(self): input_with_timeout = "certbot._internal.display.util.input_with_timeout" with mock.patch(input_with_timeout, return_value="domain.com"): code, input_ = self.displayer.input("message", force_interactive=True) assert code == display_util.OK assert input_ == "domain.com" def test_input_noninteractive(self): default = "foo" code, input_ = self._force_noninteractive( self.displayer.input, "message", default=default) assert code == display_util.OK assert input_ == default def test_input_assertion_fail(self): # If the call to util.assert_valid_call is commented out, an # error.Error is raised, otherwise, an AssertionError is raised. with pytest.raises(Exception): self._force_noninteractive(self.displayer.input, "message", cli_flag="--flag") def test_input_assertion_fail2(self): with mock.patch("certbot.display.util.assert_valid_call"): with pytest.raises(errors.Error): self._force_noninteractive(self.displayer.input, "msg", cli_flag="--flag") def test_yesno(self): input_with_timeout = "certbot._internal.display.util.input_with_timeout" with mock.patch(input_with_timeout, return_value="Yes"): assert self.displayer.yesno( "message", force_interactive=True) with mock.patch(input_with_timeout, return_value="y"): assert self.displayer.yesno( "message", force_interactive=True) with mock.patch(input_with_timeout, side_effect=["maybe", "y"]): assert self.displayer.yesno( "message", force_interactive=True) with mock.patch(input_with_timeout, return_value="No"): assert not self.displayer.yesno( "message", force_interactive=True) with mock.patch(input_with_timeout, side_effect=["cancel", "n"]): assert not self.displayer.yesno( "message", force_interactive=True) with mock.patch(input_with_timeout, return_value="a"): assert self.displayer.yesno( "msg", yes_label="Agree", force_interactive=True) def test_yesno_noninteractive(self): assert self._force_noninteractive( self.displayer.yesno, "message", default=True) @mock.patch("certbot._internal.display.util.input_with_timeout") def test_checklist_valid(self, mock_input): mock_input.return_value = "2 1" code, tag_list = self.displayer.checklist( "msg", TAGS, force_interactive=True) assert (code, set(tag_list)) == (display_util.OK, {"tag1", "tag2"}) @mock.patch("certbot._internal.display.util.input_with_timeout") def test_checklist_empty(self, mock_input): mock_input.return_value = "" code, tag_list = self.displayer.checklist("msg", TAGS, force_interactive=True) assert (code, set(tag_list)) == (display_util.OK, {"tag1", "tag2", "tag3"}) @mock.patch("certbot._internal.display.util.input_with_timeout") def test_checklist_miss_valid(self, mock_input): mock_input.side_effect = ["10", "tag1 please", "1"] ret = self.displayer.checklist("msg", TAGS, force_interactive=True) assert ret == (display_util.OK, ["tag1"]) @mock.patch("certbot._internal.display.util.input_with_timeout") def test_checklist_miss_quit(self, mock_input): mock_input.side_effect = ["10", "c"] ret = self.displayer.checklist("msg", TAGS, force_interactive=True) assert ret == (display_util.CANCEL, []) def test_checklist_noninteractive(self): default = TAGS code, input_ = self._force_noninteractive( self.displayer.checklist, "msg", TAGS, default=default) assert code == display_util.OK assert input_ == default def test_scrub_checklist_input_valid(self): # pylint: disable=protected-access indices = [ ["1"], ["1", "2", "1"], ["2", "3"], ] exp = [ {"tag1"}, {"tag1", "tag2"}, {"tag2", "tag3"}, ] for i, list_ in enumerate(indices): set_tags = set( self.displayer._scrub_checklist_input(list_, TAGS)) assert set_tags == exp[i] @mock.patch("certbot._internal.display.util.input_with_timeout") def test_directory_select(self, mock_input): args = ["msg", "/var/www/html", "--flag", True] user_input = "/var/www/html" mock_input.return_value = user_input returned = self.displayer.directory_select(*args) assert returned == (display_util.OK, user_input) def test_directory_select_noninteractive(self): default = "/var/www/html" code, input_ = self._force_noninteractive( self.displayer.directory_select, "msg", default=default) assert code == display_util.OK assert input_ == default def _force_noninteractive(self, func, *args, **kwargs): skipped_interaction = self.displayer.skipped_interaction with mock.patch("certbot._internal.display.obj.sys.stdin") as mock_stdin: mock_stdin.isatty.return_value = False with mock.patch("certbot._internal.display.obj.logger") as mock_logger: result = func(*args, **kwargs) if skipped_interaction: assert mock_logger.warning.called is False else: assert mock_logger.warning.call_count == 1 return result def test_scrub_checklist_input_invalid(self): # pylint: disable=protected-access indices = [ ["0"], ["4"], ["tag1"], ["1", "tag1"], ["2", "o"] ] for list_ in indices: assert self.displayer._scrub_checklist_input(list_, TAGS) == [] def test_print_menu(self): # pylint: disable=protected-access # This is purely cosmetic... just make sure there aren't any exceptions self.displayer._print_menu("msg", CHOICES) self.displayer._print_menu("msg", TAGS) def test_get_valid_int_ans_valid(self): # pylint: disable=protected-access input_with_timeout = "certbot._internal.display.util.input_with_timeout" with mock.patch(input_with_timeout, return_value="1"): assert self.displayer._get_valid_int_ans(1) == (display_util.OK, 1) ans = "2" with mock.patch(input_with_timeout, return_value=ans): assert self.displayer._get_valid_int_ans(3) == \ (display_util.OK, int(ans)) def test_get_valid_int_ans_invalid(self): # pylint: disable=protected-access answers = [ ["0", "c"], ["4", "one", "C"], ["c"], ] input_with_timeout = "certbot._internal.display.util.input_with_timeout" for ans in answers: with mock.patch(input_with_timeout, side_effect=ans): assert self.displayer._get_valid_int_ans(3) == \ (display_util.CANCEL, -1) class NoninteractiveDisplayTest(unittest.TestCase): """Test non-interactive display. These tests are pretty easy!""" def setUp(self): self.mock_stdout = mock.MagicMock() self.displayer = display_obj.NoninteractiveDisplay(self.mock_stdout) @mock.patch("certbot._internal.display.obj.logger") def test_notification_no_pause(self, mock_logger): self.displayer.notification("message", 10) string = self.mock_stdout.write.call_args[0][0] assert "message" in string mock_logger.debug.assert_called_with("Notifying user: %s", "message") def test_notification_decoration(self): from certbot.compat import os self.displayer.notification("message", pause=False, decorate=False) string = self.mock_stdout.write.call_args[0][0] assert string == "message" + os.linesep self.displayer.notification("message2", pause=False) string = self.mock_stdout.write.call_args[0][0] assert "- - - " in string assert "message2" + os.linesep in string def test_input(self): d = "an incomputable value" ret = self.displayer.input("message", default=d) assert ret == (display_util.OK, d) with pytest.raises(errors.MissingCommandlineFlag): self.displayer.input("message") def test_menu(self): ret = self.displayer.menu("message", CHOICES, default=1) assert ret == (display_util.OK, 1) with pytest.raises(errors.MissingCommandlineFlag): self.displayer.menu("message", CHOICES) def test_yesno(self): d = False ret = self.displayer.yesno("message", default=d) assert ret == d with pytest.raises(errors.MissingCommandlineFlag): self.displayer.yesno("message") def test_checklist(self): d = [1, 3] ret = self.displayer.checklist("message", TAGS, default=d) assert ret == (display_util.OK, d) with pytest.raises(errors.MissingCommandlineFlag): self.displayer.checklist("message", TAGS) def test_directory_select(self): default = "/var/www/html" expected = (display_util.OK, default) actual = self.displayer.directory_select("msg", default) assert expected == actual with pytest.raises(errors.MissingCommandlineFlag): self.displayer.directory_select("msg") if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/display/ops_test.py0000664000175100017510000004746314561227515022644 0ustar00ericaerica# coding=utf-8 """Test certbot.display.ops.""" import sys import unittest from unittest import mock import josepy as jose import pytest from acme import messages from certbot import errors from certbot._internal import account from certbot._internal.display import obj as display_obj from certbot.compat import filesystem from certbot.compat import os from certbot.display import ops from certbot.display import util as display_util import certbot.tests.util as test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class GetEmailTest(unittest.TestCase): """Tests for certbot.display.ops.get_email.""" @classmethod def _call(cls, **kwargs): from certbot.display.ops import get_email return get_email(**kwargs) @test_util.patch_display_util() def test_cancel_none(self, mock_get_utility): mock_input = mock_get_utility().input mock_input.return_value = (display_util.CANCEL, "foo@bar.baz") with pytest.raises(errors.Error): self._call() with pytest.raises(errors.Error): self._call(optional=False) @test_util.patch_display_util() def test_ok_safe(self, mock_get_utility): mock_input = mock_get_utility().input mock_input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.return_value = True assert self._call() == "foo@bar.baz" @test_util.patch_display_util() def test_ok_not_safe(self, mock_get_utility): mock_input = mock_get_utility().input mock_input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] assert self._call() == "foo@bar.baz" @test_util.patch_display_util() def test_invalid_flag(self, mock_get_utility): invalid_txt = "There seem to be problems" mock_input = mock_get_utility().input mock_input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self._call() assert invalid_txt not in mock_input.call_args[0][0] self._call(invalid=True) assert invalid_txt in mock_input.call_args[0][0] @test_util.patch_display_util() def test_optional_flag(self, mock_get_utility): mock_input = mock_get_utility().input mock_input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] self._call(optional=False) for call in mock_input.call_args_list: assert "--register-unsafely-without-email" not in call[0][0] @test_util.patch_display_util() def test_optional_invalid_unsafe(self, mock_get_utility): invalid_txt = "There seem to be problems" mock_input = mock_get_utility().input mock_input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] self._call(invalid=True) assert invalid_txt in mock_input.call_args[0][0] class ChooseAccountTest(test_util.TempDirTestCase): """Tests for certbot.display.ops.choose_account.""" def setUp(self): super().setUp() display_obj.set_display(display_obj.FileDisplay(sys.stdout, False)) self.account_keys_dir = os.path.join(self.tempdir, "keys") filesystem.makedirs(self.account_keys_dir, 0o700) self.config = mock.MagicMock( accounts_dir=self.tempdir, account_keys_dir=self.account_keys_dir, server="certbot-demo.org") self.key = KEY self.acc1 = account.Account(messages.RegistrationResource( uri=None, body=messages.Registration.from_data( email="email1@g.com")), self.key) self.acc2 = account.Account(messages.RegistrationResource( uri=None, body=messages.Registration.from_data( email="email2@g.com", phone="phone")), self.key) @classmethod def _call(cls, accounts): return ops.choose_account(accounts) @test_util.patch_display_util() def test_one(self, mock_util): mock_util().menu.return_value = (display_util.OK, 0) assert self._call([self.acc1]) == self.acc1 @test_util.patch_display_util() def test_two(self, mock_util): mock_util().menu.return_value = (display_util.OK, 1) assert self._call([self.acc1, self.acc2]) == self.acc2 @test_util.patch_display_util() def test_cancel(self, mock_util): mock_util().menu.return_value = (display_util.CANCEL, 1) assert self._call([self.acc1, self.acc2]) is None class GenHttpsNamesTest(unittest.TestCase): """Test _gen_https_names.""" def setUp(self): display_obj.set_display(display_obj.FileDisplay(sys.stdout, False)) @classmethod def _call(cls, domains): from certbot.display.ops import _gen_https_names return _gen_https_names(domains) def test_zero(self): assert self._call([]) == "" def test_one(self): doms = [ "example.com", "asllkjsadfljasdf.c", ] for dom in doms: assert self._call([dom]) == "https://%s" % dom def test_two(self): domains_list = [ ["foo.bar.org", "bar.org"], ["paypal.google.facebook.live.com", "*.zombo.example.com"], ] for doms in domains_list: assert self._call(doms) == \ "https://{dom[0]} and https://{dom[1]}".format(dom=doms) def test_three(self): doms = ["a.org", "b.org", "c.org"] # We use an oxford comma assert self._call(doms) == \ "https://{dom[0]}, https://{dom[1]}, and https://{dom[2]}".format( dom=doms) def test_four(self): doms = ["a.org", "b.org", "c.org", "d.org"] exp = ("https://{dom[0]}, https://{dom[1]}, https://{dom[2]}, " "and https://{dom[3]}".format(dom=doms)) assert self._call(doms) == exp class ChooseNamesTest(unittest.TestCase): """Test choose names.""" def setUp(self): display_obj.set_display(display_obj.FileDisplay(sys.stdout, False)) self.mock_install = mock.MagicMock() @classmethod def _call(cls, installer, question=None): from certbot.display.ops import choose_names return choose_names(installer, question) @mock.patch("certbot.display.ops._choose_names_manually") def test_no_installer(self, mock_manual): self._call(None) assert mock_manual.call_count == 1 @test_util.patch_display_util() def test_no_installer_cancel(self, mock_util): mock_util().input.return_value = (display_util.CANCEL, []) assert self._call(None) == [] @test_util.patch_display_util() def test_no_names_choose(self, mock_util): self.mock_install().get_all_names.return_value = set() domain = "example.com" mock_util().input.return_value = (display_util.OK, domain) actual_doms = self._call(self.mock_install) assert mock_util().input.call_count == 1 assert actual_doms == [domain] def test_sort_names_trivial(self): from certbot.display.ops import _sort_names #sort an empty list assert _sort_names([]) == [] #sort simple domains some_domains = ["ex.com", "zx.com", "ax.com"] assert _sort_names(some_domains) == ["ax.com", "ex.com", "zx.com"] #Sort subdomains of a single domain domain = ".ex.com" unsorted_short = ["e", "a", "z", "y"] unsorted_long = [us + domain for us in unsorted_short] sorted_short = sorted(unsorted_short) sorted_long = [us + domain for us in sorted_short] assert _sort_names(unsorted_long) == sorted_long def test_sort_names_many(self): from certbot.display.ops import _sort_names unsorted_domains = [".cx.com", ".bx.com", ".ax.com", ".dx.com"] unsorted_short = ["www", "bnother.long.subdomain", "a", "a.long.subdomain", "z", "b"] #Of course sorted doesn't work here ;-) sorted_short = ["a", "b", "a.long.subdomain", "bnother.long.subdomain", "www", "z"] to_sort = [] for short in unsorted_short: for domain in unsorted_domains: to_sort.append(short+domain) sortd = [] for domain in sorted(unsorted_domains): for short in sorted_short: sortd.append(short+domain) assert _sort_names(to_sort) == sortd @test_util.patch_display_util() def test_filter_names_valid_return(self, mock_util): self.mock_install.get_all_names.return_value = {"example.com"} mock_util().checklist.return_value = (display_util.OK, ["example.com"]) names = self._call(self.mock_install) assert names == ["example.com"] assert mock_util().checklist.call_count == 1 @test_util.patch_display_util() def test_filter_namees_override_question(self, mock_util): self.mock_install.get_all_names.return_value = {"example.com"} mock_util().checklist.return_value = (display_util.OK, ["example.com"]) names = self._call(self.mock_install, "Custom") assert names == ["example.com"] assert mock_util().checklist.call_count == 1 assert mock_util().checklist.call_args[0][0] == "Custom" @test_util.patch_display_util() def test_filter_names_nothing_selected(self, mock_util): self.mock_install.get_all_names.return_value = {"example.com"} mock_util().checklist.return_value = (display_util.OK, []) assert self._call(self.mock_install) == [] @test_util.patch_display_util() def test_filter_names_cancel(self, mock_util): self.mock_install.get_all_names.return_value = {"example.com"} mock_util().checklist.return_value = ( display_util.CANCEL, ["example.com"]) assert self._call(self.mock_install) == [] def test_get_valid_domains(self): from certbot.display.ops import get_valid_domains all_valid = ["example.com", "second.example.com", "also.example.com", "under_score.example.com", "justtld", "*.wildcard.com"] all_invalid = ["öóòps.net", "uniçodé.com"] two_valid = ["example.com", "úniçøde.com", "also.example.com"] assert get_valid_domains(all_valid) == all_valid assert get_valid_domains(all_invalid) == [] assert len(get_valid_domains(two_valid)) == 2 @test_util.patch_display_util() def test_choose_manually(self, mock_util): from certbot.display.ops import _choose_names_manually utility_mock = mock_util() # No retry utility_mock.yesno.return_value = False # IDN and no retry utility_mock.input.return_value = (display_util.OK, "uniçodé.com") assert _choose_names_manually() == [] # IDN exception with previous mocks with mock.patch( "certbot.display.ops.internal_display_util.separate_list_input" ) as mock_sli: unicode_error = UnicodeEncodeError('mock', u'', 0, 1, 'mock') mock_sli.side_effect = unicode_error assert _choose_names_manually() == [] # Valid domains utility_mock.input.return_value = (display_util.OK, ("example.com," "under_score.example.com," "justtld," "valid.example.com")) assert _choose_names_manually() == \ ["example.com", "under_score.example.com", "justtld", "valid.example.com"] @test_util.patch_display_util() def test_choose_manually_retry(self, mock_util): from certbot.display.ops import _choose_names_manually utility_mock = mock_util() # Three iterations utility_mock.input.return_value = (display_util.OK, "uniçodé.com") utility_mock.yesno.side_effect = [True, True, False] _choose_names_manually() assert utility_mock.yesno.call_count == 3 class SuccessInstallationTest(unittest.TestCase): """Test the success installation message.""" @classmethod def _call(cls, names): from certbot.display.ops import success_installation success_installation(names) @test_util.patch_display_util() @mock.patch("certbot.display.util.notify") def test_success_installation(self, mock_notify, mock_display): mock_display().notification.return_value = None names = ["example.com", "abc.com"] self._call(names) assert mock_notify.call_count == 1 arg = mock_notify.call_args_list[0][0][0] for name in names: assert name in arg class SuccessRenewalTest(unittest.TestCase): """Test the success renewal message.""" @classmethod def _call(cls, names): from certbot.display.ops import success_renewal success_renewal(names) @test_util.patch_display_util() @mock.patch("certbot.display.util.notify") def test_success_renewal(self, mock_notify, mock_display): mock_display().notification.return_value = None names = ["example.com", "abc.com"] self._call(names) assert mock_notify.call_count == 1 class SuccessRevocationTest(unittest.TestCase): """Test the success revocation message.""" @classmethod def _call(cls, path): from certbot.display.ops import success_revocation success_revocation(path) @test_util.patch_display_util() @mock.patch("certbot.display.util.notify") def test_success_revocation(self, mock_notify, unused_mock_display): path = "/path/to/cert.pem" self._call(path) mock_notify.assert_called_once_with( "Congratulations! You have successfully revoked the certificate " "that was located at {0}.".format(path) ) class ValidatorTests(unittest.TestCase): """Tests for `validated_input` and `validated_directory`.""" __ERROR = "Must be non-empty" valid_input = "asdf" valid_directory = "/var/www/html" @staticmethod def __validator(m): if m == "": raise errors.PluginError(ValidatorTests.__ERROR) @test_util.patch_display_util() def test_input_blank_with_validator(self, mock_util): mock_util().input.side_effect = [(display_util.OK, ""), (display_util.OK, ""), (display_util.OK, ""), (display_util.OK, self.valid_input)] returned = ops.validated_input(self.__validator, "message", force_interactive=True) assert ValidatorTests.__ERROR == mock_util().notification.call_args[0][0] assert returned == (display_util.OK, self.valid_input) @test_util.patch_display_util() def test_input_validation_with_default(self, mock_util): mock_util().input.side_effect = [(display_util.OK, self.valid_input)] returned = ops.validated_input(self.__validator, "msg", default="other") assert returned == (display_util.OK, self.valid_input) @test_util.patch_display_util() def test_input_validation_with_bad_default(self, mock_util): mock_util().input.side_effect = [(display_util.OK, self.valid_input)] with pytest.raises(AssertionError): ops.validated_input(self.__validator, "msg", default="") @test_util.patch_display_util() def test_input_cancel_with_validator(self, mock_util): mock_util().input.side_effect = [(display_util.CANCEL, "")] code, unused_raw = ops.validated_input(self.__validator, "message", force_interactive=True) assert code == display_util.CANCEL @test_util.patch_display_util() def test_directory_select_validation(self, mock_util): mock_util().directory_select.side_effect = [(display_util.OK, ""), (display_util.OK, self.valid_directory)] returned = ops.validated_directory(self.__validator, "msg", force_interactive=True) assert ValidatorTests.__ERROR == mock_util().notification.call_args[0][0] assert returned == (display_util.OK, self.valid_directory) @test_util.patch_display_util() def test_directory_select_validation_with_default(self, mock_util): mock_util().directory_select.side_effect = [(display_util.OK, self.valid_directory)] returned = ops.validated_directory(self.__validator, "msg", default="other") assert returned == (display_util.OK, self.valid_directory) @test_util.patch_display_util() def test_directory_select_validation_with_bad_default(self, mock_util): mock_util().directory_select.side_effect = [(display_util.OK, self.valid_directory)] with pytest.raises(AssertionError): ops.validated_directory(self.__validator, "msg", default="") class ChooseValuesTest(unittest.TestCase): """Test choose_values.""" @classmethod def _call(cls, values, question): from certbot.display.ops import choose_values return choose_values(values, question) @test_util.patch_display_util() def test_choose_names_success(self, mock_util): items = ["first", "second", "third"] mock_util().checklist.return_value = (display_util.OK, [items[2]]) result = self._call(items, None) assert result == [items[2]] assert mock_util().checklist.called is True assert mock_util().checklist.call_args[0][0] == "" @test_util.patch_display_util() def test_choose_names_success_question(self, mock_util): items = ["first", "second", "third"] question = "Which one?" mock_util().checklist.return_value = (display_util.OK, [items[1]]) result = self._call(items, question) assert result == [items[1]] assert mock_util().checklist.called is True assert mock_util().checklist.call_args[0][0] == question @test_util.patch_display_util() def test_choose_names_user_cancel(self, mock_util): items = ["first", "second", "third"] question = "Want to cancel?" mock_util().checklist.return_value = (display_util.CANCEL, []) result = self._call(items, question) assert result == [] assert mock_util().checklist.called is True assert mock_util().checklist.call_args[0][0] == question @mock.patch('certbot.display.ops.logger') @mock.patch('certbot.display.util.notify') class ReportExecutedCommand(unittest.TestCase): """Test report_executed_command""" @classmethod def _call(cls, cmd_name: str, rc: int, out: str, err: str): from certbot.display.ops import report_executed_command report_executed_command(cmd_name, rc, out, err) def test_mixed_success(self, mock_notify, mock_logger): self._call("some-hook", 0, "Did a thing", "Some warning") assert mock_logger.warning.call_count == 1 assert mock_notify.call_count == 1 def test_mixed_error(self, mock_notify, mock_logger): self._call("some-hook", -127, "Did a thing", "Some warning") assert mock_logger.warning.call_count == 2 assert mock_notify.call_count == 1 def test_empty_success(self, mock_notify, mock_logger): self._call("some-hook", 0, "\n", " ") assert mock_logger.warning.call_count == 0 assert mock_notify.call_count == 0 if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/display/util_test.py0000664000175100017510000000450714561227515023010 0ustar00ericaerica"""Test :mod:`certbot.display.util`.""" import io import socket import sys import tempfile from unittest import mock import pytest from certbot import errors import certbot.tests.util as test_util @test_util.patch_display_util() def test_notify(mock_util): from certbot.display.util import notify notify("Hello World") mock_util().notification.assert_called_with( "Hello World", pause=False, decorate=False, wrap=False ) @test_util.patch_display_util() def test_notification(mock_util): from certbot.display.util import notification notification("Hello World") mock_util().notification.assert_called_with( "Hello World", pause=True, decorate=True, wrap=True, force_interactive=False ) @test_util.patch_display_util() def test_menu(mock_util): from certbot.display.util import menu menu("Hello World", ["one", "two"], default=0) mock_util().menu.assert_called_with( "Hello World", ["one", "two"], default=0, cli_flag=None, force_interactive=False ) @test_util.patch_display_util() def test_input_text(mock_util): from certbot.display.util import input_text input_text("Hello World", default="something") mock_util().input.assert_called_with( "Hello World", default='something', cli_flag=None, force_interactive=False ) @test_util.patch_display_util() def test_yesno(mock_util): from certbot.display.util import yesno yesno("Hello World", default=True) mock_util().yesno.assert_called_with( "Hello World", yes_label='Yes', no_label='No', default=True, cli_flag=None, force_interactive=False ) @test_util.patch_display_util() def test_checklist(mock_util): from certbot.display.util import checklist checklist("Hello World", ["one", "two"], default="one") mock_util().checklist.assert_called_with( "Hello World", ['one', 'two'], default='one', cli_flag=None, force_interactive=False ) @test_util.patch_display_util() def test_directory_select(mock_util): from certbot.display.util import directory_select directory_select("Hello World", default="something") mock_util().directory_select.assert_called_with( "Hello World", default='something', cli_flag=None, force_interactive=False ) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/eff_test.py0000664000175100017510000001502414561227515021122 0ustar00ericaerica"""Tests for certbot._internal.eff.""" import datetime import sys import unittest from unittest import mock import josepy import pytest import pytz import requests from acme import messages from certbot._internal import account from certbot._internal import constants import certbot.tests.util as test_util _KEY = josepy.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class SubscriptionTest(test_util.ConfigTestCase): """Abstract class for subscription tests.""" def setUp(self): super().setUp() self.account = account.Account( regr=messages.RegistrationResource( uri=None, body=messages.Registration(), new_authzr_uri='hi'), key=_KEY, meta=account.Account.Meta( creation_host='test.certbot.org', creation_dt=datetime.datetime( 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC))) self.config.email = 'certbot@example.org' self.config.eff_email = None class PrepareSubscriptionTest(SubscriptionTest): """Tests for certbot._internal.eff.prepare_subscription.""" def _call(self): from certbot._internal.eff import prepare_subscription prepare_subscription(self.config, self.account) @test_util.patch_display_util() @mock.patch("certbot._internal.eff.display_util.notify") def test_failure(self, mock_notify, mock_get_utility): self.config.email = None self.config.eff_email = True self._call() actual = mock_notify.call_args[0][0] expected_part = "because you didn't provide an e-mail address" assert expected_part in actual assert self.account.meta.register_to_eff is None @test_util.patch_display_util() def test_will_not_subscribe_with_no_prompt(self, mock_get_utility): self.config.eff_email = False self._call() self._assert_no_get_utility_calls(mock_get_utility) assert self.account.meta.register_to_eff is None @test_util.patch_display_util() def test_will_subscribe_with_no_prompt(self, mock_get_utility): self.config.eff_email = True self._call() self._assert_no_get_utility_calls(mock_get_utility) assert self.account.meta.register_to_eff == self.config.email @test_util.patch_display_util() def test_will_not_subscribe_with_prompt(self, mock_get_utility): mock_get_utility().yesno.return_value = False self._call() assert not mock_get_utility().add_message.called self._assert_correct_yesno_call(mock_get_utility) assert self.account.meta.register_to_eff is None @test_util.patch_display_util() def test_will_subscribe_with_prompt(self, mock_get_utility): mock_get_utility().yesno.return_value = True self._call() assert not mock_get_utility().add_message.called self._assert_correct_yesno_call(mock_get_utility) assert self.account.meta.register_to_eff == self.config.email def _assert_no_get_utility_calls(self, mock_get_utility): assert not mock_get_utility().yesno.called assert not mock_get_utility().add_message.called def _assert_correct_yesno_call(self, mock_get_utility): assert mock_get_utility().yesno.called call_args, call_kwargs = mock_get_utility().yesno.call_args actual = call_args[0] expected_part = 'Electronic Frontier Foundation' assert expected_part in actual assert not call_kwargs.get('default', True) class HandleSubscriptionTest(SubscriptionTest): """Tests for certbot._internal.eff.handle_subscription.""" def _call(self): from certbot._internal.eff import handle_subscription handle_subscription(self.config, self.account) @mock.patch('certbot._internal.eff.subscribe') def test_no_subscribe(self, mock_subscribe): self._call() assert mock_subscribe.called is False @mock.patch('certbot._internal.eff.subscribe') def test_subscribe(self, mock_subscribe): self.account.meta = self.account.meta.update(register_to_eff=self.config.email) self._call() assert mock_subscribe.called assert mock_subscribe.call_args[0][0] == self.config.email class SubscribeTest(unittest.TestCase): """Tests for certbot._internal.eff.subscribe.""" def setUp(self): self.email = 'certbot@example.org' self.json = {'status': True} self.response = mock.Mock(ok=True) self.response.json.return_value = self.json patcher = mock.patch("certbot._internal.eff.display_util.notify") self.mock_notify = patcher.start() self.addCleanup(patcher.stop) @mock.patch('certbot._internal.eff.requests.post') def _call(self, mock_post): mock_post.return_value = self.response from certbot._internal.eff import subscribe subscribe(self.email) self._check_post_call(mock_post) def _check_post_call(self, mock_post): assert mock_post.call_count == 1 call_args, call_kwargs = mock_post.call_args assert call_args[0] == constants.EFF_SUBSCRIBE_URI data = call_kwargs.get('data') assert data is not None assert data.get('email') == self.email def test_bad_status(self): self.json['status'] = False self._call() actual = self._get_reported_message() expected_part = 'because your e-mail address appears to be invalid.' assert expected_part in actual def test_not_ok(self): self.response.ok = False self.response.raise_for_status.side_effect = requests.exceptions.HTTPError self._call() actual = self._get_reported_message() unexpected_part = 'because' assert unexpected_part not in actual def test_response_not_json(self): self.response.json.side_effect = ValueError() self._call() actual = self._get_reported_message() expected_part = 'problem' assert expected_part in actual def test_response_json_missing_status_element(self): self.json.clear() self._call() actual = self._get_reported_message() expected_part = 'problem' assert expected_part in actual def _get_reported_message(self): assert self.mock_notify.called return self.mock_notify.call_args[0][0] @test_util.patch_display_util() def test_subscribe(self, mock_get_utility): self._call() assert mock_get_utility.called is False if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/error_handler_test.py0000664000175100017510000001212214561227515023204 0ustar00ericaerica"""Tests for certbot._internal.error_handler.""" import contextlib import signal import sys from typing import Callable from typing import Dict from typing import Union import unittest from unittest import mock import pytest from certbot.compat import os def get_signals(signums): """Get the handlers for an iterable of signums.""" return {s: signal.getsignal(s) for s in signums} def set_signals(sig_handler_dict): """Set the signal (keys) with the handler (values) from the input dict.""" for s, h in sig_handler_dict.items(): signal.signal(s, h) @contextlib.contextmanager def signal_receiver(signums): """Context manager to catch signals""" signals = [] prev_handlers: Dict[int, Union[int, None, Callable]] = get_signals(signums) set_signals({s: lambda s, _: signals.append(s) for s in signums}) yield signals set_signals(prev_handlers) def send_signal(signum): """Send the given signal""" os.kill(os.getpid(), signum) class ErrorHandlerTest(unittest.TestCase): """Tests for certbot._internal.error_handler.ErrorHandler.""" def setUp(self): from certbot._internal import error_handler self.init_func = mock.MagicMock() self.init_args = {42,} self.init_kwargs = {'foo': 'bar'} self.handler = error_handler.ErrorHandler(self.init_func, *self.init_args, **self.init_kwargs) # pylint: disable=protected-access self.signals = error_handler._SIGNALS def test_context_manager(self): exception_raised = False try: with self.handler: raise ValueError except ValueError: exception_raised = True assert exception_raised self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) def test_context_manager_with_signal(self): if not self.signals: self.skipTest(reason='Signals cannot be handled on Windows.') init_signals = get_signals(self.signals) with signal_receiver(self.signals) as signals_received: with self.handler: should_be_42 = 42 send_signal(self.signals[0]) should_be_42 *= 10 # check execution stopped when the signal was sent assert 42 == should_be_42 # assert signals were caught assert [self.signals[0]] == signals_received # assert the error handling function was just called once self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) for signum in self.signals: assert init_signals[signum] == signal.getsignal(signum) def test_bad_recovery(self): bad_func = mock.MagicMock(side_effect=[ValueError]) self.handler.register(bad_func) try: with self.handler: raise ValueError except ValueError: pass self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) bad_func.assert_called_once_with() def test_bad_recovery_with_signal(self): if not self.signals: self.skipTest(reason='Signals cannot be handled on Windows.') sig1 = self.signals[0] sig2 = self.signals[-1] bad_func = mock.MagicMock(side_effect=lambda: send_signal(sig1)) self.handler.register(bad_func) with signal_receiver(self.signals) as signals_received: with self.handler: send_signal(sig2) assert [sig2, sig1] == signals_received self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) bad_func.assert_called_once_with() def test_sysexit_ignored(self): try: with self.handler: sys.exit(0) except SystemExit: pass assert self.init_func.called is False def test_regular_exit(self): func = mock.MagicMock() self.handler.register(func) with self.handler: pass self.init_func.assert_not_called() func.assert_not_called() class ExitHandlerTest(ErrorHandlerTest): """Tests for certbot._internal.error_handler.ExitHandler.""" def setUp(self): from certbot._internal import error_handler super().setUp() self.handler = error_handler.ExitHandler(self.init_func, *self.init_args, **self.init_kwargs) def test_regular_exit(self): func = mock.MagicMock() self.handler.register(func) with self.handler: pass self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) func.assert_called_once_with() if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/errors_test.py0000664000175100017510000000364414561227515021703 0ustar00ericaerica"""Tests for certbot.errors.""" import sys import unittest from unittest import mock import pytest from acme import messages from certbot import achallenges from certbot.tests import acme_util class FailedChallengesTest(unittest.TestCase): """Tests for certbot.errors.FailedChallenges.""" def setUp(self): from certbot.errors import FailedChallenges self.error = FailedChallenges({achallenges.DNS( domain="example.com", challb=messages.ChallengeBody( chall=acme_util.DNS01, uri=None, error=messages.Error.with_code("tls", detail="detail")))}) def test_str(self): assert str(self.error).startswith( "Failed authorization procedure. example.com (dns-01): " "urn:ietf:params:acme:error:tls") def test_unicode(self): from certbot.errors import FailedChallenges arabic_detail = u'\u0639\u062f\u0627\u0644\u0629' arabic_error = FailedChallenges({achallenges.DNS( domain="example.com", challb=messages.ChallengeBody( chall=acme_util.DNS01, uri=None, error=messages.Error.with_code("tls", detail=arabic_detail)))}) assert str(arabic_error).startswith( "Failed authorization procedure. example.com (dns-01): " "urn:ietf:params:acme:error:tls") class StandaloneBindErrorTest(unittest.TestCase): """Tests for certbot.errors.StandaloneBindError.""" def setUp(self): from certbot.errors import StandaloneBindError self.error = StandaloneBindError(mock.sentinel.error, 1234) def test_instance_args(self): assert mock.sentinel.error == self.error.socket_error assert 1234 == self.error.port def test_str(self): assert str(self.error).startswith( "Problem binding to port 1234: ") if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/helpful_test.py0000664000175100017510000001156714561227515022031 0ustar00ericaerica"""Tests for certbot.helpful_parser""" import sys import pytest from certbot._internal.cli import HelpfulArgumentParser class TestScanningFlags: '''Test the prescan_for_flag method of HelpfulArgumentParser''' def test_prescan_no_help_flag(self): arg_parser = HelpfulArgumentParser(['run'], {}) detected_flag = arg_parser.prescan_for_flag('--help', ['all', 'certonly']) assert detected_flag is False detected_flag = arg_parser.prescan_for_flag('-h', ['all, certonly']) assert detected_flag is False def test_prescan_unvalid_topic(self): arg_parser = HelpfulArgumentParser(['--help', 'all'], {}) detected_flag = arg_parser.prescan_for_flag('--help', ['potato']) assert detected_flag is True detected_flag = arg_parser.prescan_for_flag('-h', arg_parser.help_topics) assert detected_flag is False def test_prescan_valid_topic(self): arg_parser = HelpfulArgumentParser(['-h', 'all'], {}) detected_flag = arg_parser.prescan_for_flag('-h', arg_parser.help_topics) assert detected_flag == 'all' detected_flag = arg_parser.prescan_for_flag('--help', arg_parser.help_topics) assert detected_flag is False class TestDetermineVerbs: '''Tests for determine_verb methods of HelpfulArgumentParser''' def test_determine_verb_wrong_verb(self): arg_parser = HelpfulArgumentParser(['potato'], {}) assert arg_parser.verb == "run" assert arg_parser.args == ["potato"] def test_determine_verb_help(self): arg_parser = HelpfulArgumentParser(['--help', 'everything'], {}) assert arg_parser.verb == "help" assert arg_parser.args == ["--help", "everything"] arg_parser = HelpfulArgumentParser(['-d', 'some_domain', '--help', 'all'], {}) assert arg_parser.verb == "help" assert arg_parser.args == ['-d', 'some_domain', '--help', 'all'] def test_determine_verb(self): arg_parser = HelpfulArgumentParser(['certonly'], {}) assert arg_parser.verb == 'certonly' assert arg_parser.args == [] arg_parser = HelpfulArgumentParser(['auth'], {}) assert arg_parser.verb == 'certonly' assert arg_parser.args == [] arg_parser = HelpfulArgumentParser(['everything'], {}) assert arg_parser.verb == 'run' assert arg_parser.args == [] class TestAdd: '''Tests for add method in HelpfulArgumentParser''' def test_add_trivial_argument(self): arg_parser = HelpfulArgumentParser(['run'], {}) arg_parser.add(None, "--hello-world") parsed_args = arg_parser.parser.parse_args(['--hello-world', 'Hello World!']) assert parsed_args.hello_world == 'Hello World!' assert not hasattr(parsed_args, 'potato') def test_add_expected_argument(self): arg_parser = HelpfulArgumentParser(['--help', 'run'], {}) arg_parser.add( [None, "run", "certonly", "register"], "--eab-kid", dest="eab_kid", action="store", metavar="EAB_KID", help="Key Identifier for External Account Binding") parsed_args = arg_parser.parser.parse_args(["--eab-kid", None]) assert parsed_args.eab_kid is None assert hasattr(parsed_args, 'eab_kid') class TestAddGroup: '''Test add_group method of HelpfulArgumentParser''' def test_add_group_no_input(self): arg_parser = HelpfulArgumentParser(['run'], {}) with pytest.raises(TypeError): arg_parser.add_group() def test_add_group_topic_not_visible(self): # The user request help on run. A topic that given somewhere in the # args won't be added to the groups in the parser. arg_parser = HelpfulArgumentParser(['--help', 'run'], {}) arg_parser.add_group("auth", description="description of auth") assert arg_parser.groups == {} def test_add_group_topic_requested_help(self): arg_parser = HelpfulArgumentParser(['--help', 'run'], {}) arg_parser.add_group("run", description="description of run") assert arg_parser.groups["run"] arg_parser.add_group("certonly", description="description of certonly") with pytest.raises(KeyError): assert arg_parser.groups["certonly"] is False if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/hook_test.py0000664000175100017510000004303414561227515021324 0ustar00ericaerica"""Tests for certbot._internal.hooks.""" import sys import unittest from platform import python_version_tuple from unittest import mock import pytest from certbot import errors from certbot import util from certbot.compat import filesystem from certbot.compat import os from certbot.tests import util as test_util from typing import List def pyver_lt(major: int, minor: int): pymajor = int(python_version_tuple()[0]) pyminor = int(python_version_tuple()[1]) if pymajor < major: return True elif pymajor > major: return False else: return pyminor < minor class ValidateHooksTest(unittest.TestCase): """Tests for certbot._internal.hooks.validate_hooks.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.hooks import validate_hooks return validate_hooks(*args, **kwargs) @mock.patch("certbot._internal.hooks.validate_hook") def test_it(self, mock_validate_hook): config = mock.MagicMock() self._call(config) types = [call[0][1] for call in mock_validate_hook.call_args_list] assert {"pre", "post", "deploy",} == set(types[:-1]) # This ensures error messages are about deploy hooks when appropriate assert "renew" == types[-1] class ValidateHookTest(test_util.TempDirTestCase): """Tests for certbot._internal.hooks.validate_hook.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.hooks import validate_hook return validate_hook(*args, **kwargs) def test_hook_not_executable(self): # prevent unnecessary modifications to PATH with mock.patch("certbot._internal.hooks.plug_util.path_surgery"): # We just mock out filesystem.is_executable since on Windows, it is difficult # to get a fully working test around executable permissions. See # certbot.tests.compat.filesystem::NotExecutableTest for more in-depth tests. with mock.patch("certbot._internal.hooks.filesystem.is_executable", return_value=False): with pytest.raises(errors.HookCommandNotFound): self._call('dummy', "foo") @mock.patch("certbot._internal.hooks.util.exe_exists") def test_not_found(self, mock_exe_exists): mock_exe_exists.return_value = False with mock.patch("certbot._internal.hooks.plug_util.path_surgery") as mock_ps: with pytest.raises(errors.HookCommandNotFound): self._call("foo", "bar") assert mock_ps.called @mock.patch("certbot._internal.hooks._prog") def test_unset(self, mock_prog): self._call(None, "foo") assert mock_prog.called is False class HookTest(test_util.ConfigTestCase): """Common base class for hook tests.""" @classmethod def _call(cls, *args, **kwargs): # pragma: no cover """Calls the method being tested with the given arguments.""" raise NotImplementedError @classmethod def _call_with_mock_execute(cls, *args, **kwargs): """Calls self._call after mocking out certbot.compat.misc.execute_command_status. The mock execute object is returned rather than the return value of self._call. """ with mock.patch("certbot.compat.misc.execute_command_status") as mock_execute: mock_execute.return_value = (0, "", "") cls._call(*args, **kwargs) return mock_execute class PreHookTest(HookTest): """Tests for certbot._internal.hooks.pre_hook.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.hooks import pre_hook return pre_hook(*args, **kwargs) def setUp(self): super().setUp() self.config.pre_hook = "foo" filesystem.makedirs(self.config.renewal_pre_hooks_dir) self.dir_hook = os.path.join(self.config.renewal_pre_hooks_dir, "bar") create_hook(self.dir_hook) # Reset this value as it may have been modified by past tests self._reset_pre_hook_already() def tearDown(self): # Reset this value so it's unmodified for future tests self._reset_pre_hook_already() super().tearDown() def _reset_pre_hook_already(self): from certbot._internal.hooks import executed_pre_hooks executed_pre_hooks.clear() def test_certonly(self): self.config.verb = "certonly" self._test_nonrenew_common() def test_run(self): self.config.verb = "run" self._test_nonrenew_common() def _test_nonrenew_common(self): mock_execute = self._call_with_mock_execute(self.config) mock_execute.assert_called_once_with("pre-hook", self.config.pre_hook, env=mock.ANY) self._test_no_executions_common() def test_no_hooks(self): self.config.pre_hook = None self.config.verb = "renew" os.remove(self.dir_hook) with mock.patch("certbot._internal.hooks.logger") as mock_logger: mock_execute = self._call_with_mock_execute(self.config) assert mock_execute.called is False assert mock_logger.info.called is False def test_renew_disabled_dir_hooks(self): self.config.directory_hooks = False mock_execute = self._call_with_mock_execute(self.config) mock_execute.assert_called_once_with("pre-hook", self.config.pre_hook, env=mock.ANY) self._test_no_executions_common() def test_renew_no_overlap(self): self.config.verb = "renew" mock_execute = self._call_with_mock_execute(self.config) mock_execute.assert_any_call("pre-hook", self.dir_hook, env=mock.ANY) mock_execute.assert_called_with("pre-hook", self.config.pre_hook, env=mock.ANY) self._test_no_executions_common() def test_renew_with_overlap(self): self.config.pre_hook = self.dir_hook self.config.verb = "renew" mock_execute = self._call_with_mock_execute(self.config) mock_execute.assert_called_once_with("pre-hook", self.dir_hook, env=mock.ANY) self._test_no_executions_common() def _test_no_executions_common(self): with mock.patch("certbot._internal.hooks.logger") as mock_logger: mock_execute = self._call_with_mock_execute(self.config) assert mock_execute.called is False assert mock_logger.info.called class PostHookTest(HookTest): """Tests for certbot._internal.hooks.post_hook.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.hooks import post_hook return post_hook(*args, **kwargs) def setUp(self): super().setUp() self.config.post_hook = "bar" filesystem.makedirs(self.config.renewal_post_hooks_dir) self.dir_hook = os.path.join(self.config.renewal_post_hooks_dir, "foo") create_hook(self.dir_hook) # Reset this value as it may have been modified by past tests self._reset_post_hook_eventually() def tearDown(self): # Reset this value so it's unmodified for future tests self._reset_post_hook_eventually() super().tearDown() def _reset_post_hook_eventually(self): from certbot._internal.hooks import post_hooks del post_hooks[:] def test_certonly_and_run_with_hook(self): for verb in ("certonly", "run",): self.config.verb = verb mock_execute = self._call_with_mock_execute(self.config, []) mock_execute.assert_called_once_with("post-hook", self.config.post_hook, env=mock.ANY) assert not self._get_eventually() def test_cert_only_and_run_without_hook(self): self.config.post_hook = None for verb in ("certonly", "run",): self.config.verb = verb assert not self._call_with_mock_execute(self.config, []).called assert not self._get_eventually() @unittest.skipIf(pyver_lt(3, 8), "Python 3.8+ required for this test.") def test_renew_env(self): self.config.verb = "certonly" args = self._call_with_mock_execute(self.config, ["success.org"]).call_args assert args.kwargs['env']["RENEWED_DOMAINS"] == "success.org" def test_renew_disabled_dir_hooks(self): self.config.directory_hooks = False self._test_renew_common([self.config.post_hook]) def test_renew_no_config_hook(self): self.config.post_hook = None self._test_renew_common([self.dir_hook]) def test_renew_no_dir_hook(self): os.remove(self.dir_hook) self._test_renew_common([self.config.post_hook]) def test_renew_no_hooks(self): self.config.post_hook = None os.remove(self.dir_hook) self._test_renew_common([]) def test_renew_no_overlap(self): expected = [self.dir_hook, self.config.post_hook] self._test_renew_common(expected) self.config.post_hook = "baz" expected.append(self.config.post_hook) self._test_renew_common(expected) def test_renew_with_overlap(self): self.config.post_hook = self.dir_hook self._test_renew_common([self.dir_hook]) def _test_renew_common(self, expected): self.config.verb = "renew" for _ in range(2): self._call(self.config, []) assert self._get_eventually() == expected def _get_eventually(self): from certbot._internal.hooks import post_hooks return post_hooks class RunSavedPostHooksTest(HookTest): """Tests for certbot._internal.hooks.run_saved_post_hooks.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.hooks import run_saved_post_hooks renewed_domains = kwargs["renewed_domains"] if "renewed_domains" in kwargs else args[0] failed_domains = kwargs["failed_domains"] if "failed_domains" in kwargs else args[1] return run_saved_post_hooks(renewed_domains, failed_domains) def _call_with_mock_execute_and_eventually(self, *args, **kwargs): """Call run_saved_post_hooks but mock out execute and eventually certbot._internal.hooks.post_hooks is replaced with self.eventually. The mock execute object is returned rather than the return value of run_saved_post_hooks. """ eventually_path = "certbot._internal.hooks.post_hooks" with mock.patch(eventually_path, new=self.eventually): return self._call_with_mock_execute(*args, **kwargs) def setUp(self): super().setUp() self.eventually: List[str] = [] def test_empty(self): assert not self._call_with_mock_execute_and_eventually([], []).called def test_multiple(self): self.eventually = ["foo", "bar", "baz", "qux"] mock_execute = self._call_with_mock_execute_and_eventually([], []) calls = mock_execute.call_args_list for actual_call, expected_arg in zip(calls, self.eventually): assert actual_call[0][1] == expected_arg def test_single(self): self.eventually = ["foo"] mock_execute = self._call_with_mock_execute_and_eventually([], []) mock_execute.assert_called_once_with("post-hook", self.eventually[0], env=mock.ANY) @unittest.skipIf(pyver_lt(3, 8), "Python 3.8+ required for this test.") def test_env(self): self.eventually = ["foo"] mock_execute = self._call_with_mock_execute_and_eventually(["success.org"], ["failed.org"]) assert mock_execute.call_args.kwargs['env']["RENEWED_DOMAINS"] == "success.org" assert mock_execute.call_args.kwargs['env']["FAILED_DOMAINS"] == "failed.org" class RenewalHookTest(HookTest): """Common base class for testing deploy/renew hooks.""" # Needed for https://github.com/PyCQA/pylint/issues/179 # pylint: disable=abstract-method def _call_with_mock_execute(self, *args, **kwargs): """Calls self._call after mocking out certbot.compat.misc.execute_command_status. The mock execute object is returned rather than the return value of self._call. The mock execute object asserts that environment variables were properly set. """ domains = kwargs["domains"] if "domains" in kwargs else args[1] lineage = kwargs["lineage"] if "lineage" in kwargs else args[2] def execute_side_effect(*unused_args, **unused_kwargs): """Assert environment variables are properly set. :returns: two strings imitating no output from the hook :rtype: `tuple` of `str` """ assert os.environ["RENEWED_DOMAINS"] == " ".join(domains) assert os.environ["RENEWED_LINEAGE"] == lineage return (0, "", "") with mock.patch("certbot.compat.misc.execute_command_status") as mock_execute: mock_execute.side_effect = execute_side_effect self._call(*args, **kwargs) return mock_execute def setUp(self): super().setUp() self.vars_to_clear = { var for var in ("RENEWED_DOMAINS", "RENEWED_LINEAGE",) if var not in os.environ } def tearDown(self): for var in self.vars_to_clear: os.environ.pop(var, None) super().tearDown() class DeployHookTest(RenewalHookTest): """Tests for certbot._internal.hooks.deploy_hook.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.hooks import deploy_hook return deploy_hook(*args, **kwargs) @mock.patch("certbot._internal.hooks.logger") def test_dry_run(self, mock_logger): self.config.deploy_hook = "foo" self.config.dry_run = True mock_execute = self._call_with_mock_execute( self.config, ["example.org"], "/foo/bar") assert mock_execute.called is False assert mock_logger.info.called @mock.patch("certbot._internal.hooks.logger") def test_no_hook(self, mock_logger): self.config.deploy_hook = None mock_execute = self._call_with_mock_execute( self.config, ["example.org"], "/foo/bar") assert mock_execute.called is False assert mock_logger.info.called is False def test_success(self): domains = ["example.org", "example.net"] lineage = "/foo/bar" self.config.deploy_hook = "foo" mock_execute = self._call_with_mock_execute( self.config, domains, lineage) mock_execute.assert_called_once_with("deploy-hook", self.config.deploy_hook, env=mock.ANY) class RenewHookTest(RenewalHookTest): """Tests for certbot._internal.hooks.renew_hook""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.hooks import renew_hook return renew_hook(*args, **kwargs) def setUp(self): super().setUp() self.config.renew_hook = "foo" filesystem.makedirs(self.config.renewal_deploy_hooks_dir) self.dir_hook = os.path.join(self.config.renewal_deploy_hooks_dir, "bar") create_hook(self.dir_hook) def test_disabled_dir_hooks(self): self.config.directory_hooks = False mock_execute = self._call_with_mock_execute( self.config, ["example.org"], "/foo/bar") mock_execute.assert_called_once_with("deploy-hook", self.config.renew_hook, env=mock.ANY) @mock.patch("certbot._internal.hooks.logger") def test_dry_run(self, mock_logger): self.config.dry_run = True mock_execute = self._call_with_mock_execute( self.config, ["example.org"], "/foo/bar") assert mock_execute.called is False assert mock_logger.info.call_count == 2 def test_no_hooks(self): self.config.renew_hook = None os.remove(self.dir_hook) with mock.patch("certbot._internal.hooks.logger") as mock_logger: mock_execute = self._call_with_mock_execute( self.config, ["example.org"], "/foo/bar") assert mock_execute.called is False assert mock_logger.info.called is False def test_overlap(self): self.config.renew_hook = self.dir_hook mock_execute = self._call_with_mock_execute( self.config, ["example.net", "example.org"], "/foo/bar") mock_execute.assert_called_once_with("deploy-hook", self.dir_hook, env=mock.ANY) def test_no_overlap(self): mock_execute = self._call_with_mock_execute( self.config, ["example.org"], "/foo/bar") mock_execute.assert_any_call("deploy-hook", self.dir_hook, env=mock.ANY) mock_execute.assert_called_with("deploy-hook", self.config.renew_hook, env=mock.ANY) class ListHooksTest(test_util.TempDirTestCase): """Tests for certbot._internal.hooks.list_hooks.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.hooks import list_hooks return list_hooks(*args, **kwargs) def test_empty(self): assert not self._call(self.tempdir) def test_multiple(self): names = sorted( os.path.join(self.tempdir, basename) for basename in ("foo", "bar", "baz", "qux") ) for name in names: create_hook(name) assert self._call(self.tempdir) == names def test_single(self): name = os.path.join(self.tempdir, "foo") create_hook(name) assert self._call(self.tempdir) == [name] def test_ignore_tilde(self): name = os.path.join(self.tempdir, "foo~") create_hook(name) assert self._call(self.tempdir) == [] def create_hook(file_path): """Creates an executable file at the specified path. :param str file_path: path to create the file at """ util.safe_open(file_path, mode="w", chmod=0o744).close() if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/lock_test.py0000664000175100017510000001170414561227515021313 0ustar00ericaerica"""Tests for certbot._internal.lock.""" import functools import multiprocessing import sys import unittest from unittest import mock import pytest from certbot import errors from certbot.compat import os from certbot.tests import util as test_util try: import fcntl # pylint: disable=import-error,unused-import except ImportError: POSIX_MODE = False else: POSIX_MODE = True class LockDirTest(test_util.TempDirTestCase): """Tests for certbot._internal.lock.lock_dir.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.lock import lock_dir return lock_dir(*args, **kwargs) def test_it(self): assert_raises = functools.partial( self.assertRaises, errors.LockError, self._call, self.tempdir) lock_path = os.path.join(self.tempdir, '.certbot.lock') test_util.lock_and_call(assert_raises, lock_path) class LockFileTest(test_util.TempDirTestCase): """Tests for certbot._internal.lock.LockFile.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.lock import LockFile return LockFile(*args, **kwargs) def setUp(self): super().setUp() self.lock_path = os.path.join(self.tempdir, 'test.lock') def test_acquire_without_deletion(self): # acquire the lock in another process but don't delete the file child = multiprocessing.Process(target=self._call, args=(self.lock_path,)) child.start() child.join() assert child.exitcode == 0 assert os.path.exists(self.lock_path) # Test we're still able to properly acquire and release the lock self.test_removed() def test_contention(self): assert_raises = functools.partial( self.assertRaises, errors.LockError, self._call, self.lock_path) test_util.lock_and_call(assert_raises, self.lock_path) def test_locked_repr(self): lock_file = self._call(self.lock_path) try: locked_repr = repr(lock_file) self._test_repr_common(lock_file, locked_repr) assert 'acquired' in locked_repr finally: lock_file.release() def test_released_repr(self): lock_file = self._call(self.lock_path) lock_file.release() released_repr = repr(lock_file) self._test_repr_common(lock_file, released_repr) assert 'released' in released_repr def _test_repr_common(self, lock_file, lock_repr): assert lock_file.__class__.__name__ in lock_repr assert self.lock_path in lock_repr @test_util.skip_on_windows( 'Race conditions on lock are specific to the non-blocking file access approach on Linux.') def test_race(self): should_delete = [True, False] # Normally os module should not be imported in certbot codebase except in certbot.compat # for the sake of compatibility over Windows and Linux. # We make an exception here, since test_race is a test function called only on Linux. from os import stat # pylint: disable=os-module-forbidden def delete_and_stat(path): """Wrap os.stat and maybe delete the file first.""" if path == self.lock_path and should_delete.pop(0): os.remove(path) return stat(path) with mock.patch('certbot._internal.lock.filesystem.os.stat') as mock_stat: mock_stat.side_effect = delete_and_stat self._call(self.lock_path) assert len(should_delete) == 0 def test_removed(self): lock_file = self._call(self.lock_path) lock_file.release() assert not os.path.exists(self.lock_path) def test_unexpected_lockf_or_locking_err(self): if POSIX_MODE: mocked_function = 'certbot._internal.lock.fcntl.lockf' else: mocked_function = 'certbot._internal.lock.msvcrt.locking' msg = 'hi there' with mock.patch(mocked_function) as mock_lock: mock_lock.side_effect = IOError(msg) try: self._call(self.lock_path) except IOError as err: assert msg in str(err) else: # pragma: no cover self.fail('IOError not raised') def test_unexpected_os_err(self): if POSIX_MODE: mock_function = 'certbot._internal.lock.filesystem.os.stat' else: mock_function = 'certbot._internal.lock.msvcrt.locking' # The only expected errno are ENOENT and EACCES in lock module. msg = 'hi there' with mock.patch(mock_function) as mock_os: mock_os.side_effect = OSError(msg) try: self._call(self.lock_path) except OSError as err: assert msg in str(err) else: # pragma: no cover self.fail('OSError not raised') if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/log_test.py0000664000175100017510000003711514561227515021150 0ustar00ericaerica"""Tests for certbot._internal.log.""" import io import logging import logging.handlers import sys import time from typing import Optional import unittest from unittest import mock import pytest from acme import messages from certbot import errors from certbot import util from certbot._internal import constants from certbot.compat import filesystem from certbot.compat import os from certbot.tests import util as test_util class PreArgParseSetupTest(unittest.TestCase): """Tests for certbot._internal.log.pre_arg_parse_setup.""" @classmethod def _call(cls, *args, **kwargs): # pylint: disable=unused-argument from certbot._internal.log import pre_arg_parse_setup return pre_arg_parse_setup() def tearDown(self): # We need to call logging.shutdown() at the end of this test to # properly clean up any resources created by pre_arg_parse_setup. logging.shutdown() super().tearDown() @mock.patch('certbot._internal.log.sys') @mock.patch('certbot._internal.log.pre_arg_parse_except_hook') @mock.patch('certbot._internal.log.logging.getLogger') @mock.patch('certbot._internal.log.util.atexit_register') def test_it(self, mock_register, mock_get, mock_except_hook, mock_sys): mock_sys.argv = ['--debug'] mock_sys.version_info = sys.version_info self._call() mock_root_logger = mock_get() mock_root_logger.setLevel.assert_called_once_with(logging.DEBUG) assert mock_root_logger.addHandler.call_count == 2 memory_handler: Optional[logging.handlers.MemoryHandler] = None for call in mock_root_logger.addHandler.call_args_list: handler = call[0][0] if memory_handler is None and isinstance(handler, logging.handlers.MemoryHandler): memory_handler = handler target = memory_handler.target else: assert isinstance(handler, logging.StreamHandler) assert isinstance(target, logging.StreamHandler) mock_register.assert_called_once_with(logging.shutdown) mock_sys.excepthook(1, 2, 3) mock_except_hook.assert_called_once_with( memory_handler, 1, 2, 3, debug=True, quiet=False, log_path=mock.ANY) class PostArgParseSetupTest(test_util.ConfigTestCase): """Tests for certbot._internal.log.post_arg_parse_setup.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.log import post_arg_parse_setup return post_arg_parse_setup(*args, **kwargs) def setUp(self): super().setUp() self.config.debug = False self.config.max_log_backups = 1000 self.config.quiet = False self.config.verbose_count = constants.CLI_DEFAULTS['verbose_count'] self.devnull = open(os.devnull, 'w') from certbot._internal.log import ColoredStreamHandler self.stream_handler = ColoredStreamHandler(io.StringIO()) from certbot._internal.log import MemoryHandler from certbot._internal.log import TempHandler self.temp_handler = TempHandler() self.temp_path = self.temp_handler.path self.memory_handler = MemoryHandler(self.temp_handler) self.root_logger = mock.MagicMock( handlers=[self.memory_handler, self.stream_handler]) def tearDown(self): self.memory_handler.close() self.stream_handler.close() self.temp_handler.close() self.devnull.close() super().tearDown() def test_common(self): with mock.patch('certbot._internal.log.logging.getLogger') as mock_get_logger: mock_get_logger.return_value = self.root_logger except_hook_path = 'certbot._internal.log.post_arg_parse_except_hook' with mock.patch(except_hook_path) as mock_except_hook: with mock.patch('certbot._internal.log.sys') as mock_sys: mock_sys.version_info = sys.version_info self._call(self.config) log_path = os.path.join(self.config.logs_dir, 'letsencrypt.log') self.root_logger.removeHandler.assert_called_once_with( self.memory_handler) assert self.root_logger.addHandler.called assert os.path.exists(log_path) assert not os.path.exists(self.temp_path) mock_sys.excepthook(1, 2, 3) mock_except_hook.assert_called_once_with( 1, 2, 3, debug=self.config.debug, quiet=self.config.quiet, log_path=log_path) level = self.stream_handler.level if self.config.quiet: assert level == constants.QUIET_LOGGING_LEVEL else: assert level == constants.DEFAULT_LOGGING_LEVEL def test_debug(self): self.config.debug = True self.test_common() def test_quiet(self): self.config.quiet = True self.test_common() class SetupLogFileHandlerTest(test_util.ConfigTestCase): """Tests for certbot._internal.log.setup_log_file_handler.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.log import setup_log_file_handler return setup_log_file_handler(*args, **kwargs) def setUp(self): super().setUp() self.config.max_log_backups = 42 @mock.patch('certbot._internal.main.logging.handlers.RotatingFileHandler') def test_failure(self, mock_handler): mock_handler.side_effect = IOError try: self._call(self.config, 'test.log', '%(message)s') except errors.Error as err: assert '--logs-dir' in str(err) else: # pragma: no cover self.fail('Error not raised.') def test_success_with_rollover(self): self._test_success_common(should_rollover=True) def test_success_without_rollover(self): self.config.max_log_backups = 0 self._test_success_common(should_rollover=False) def _test_success_common(self, should_rollover): log_file = 'test.log' handler, log_path = self._call(self.config, log_file, '%(message)s') handler.close() assert handler.level == logging.DEBUG assert handler.formatter.converter == time.localtime expected_path = os.path.join(self.config.logs_dir, log_file) assert log_path == expected_path backup_path = os.path.join(self.config.logs_dir, log_file + '.1') assert os.path.exists(backup_path) == should_rollover @mock.patch('certbot._internal.log.logging.handlers.RotatingFileHandler') def test_max_log_backups_used(self, mock_handler): self._call(self.config, 'test.log', '%(message)s') backup_count = mock_handler.call_args[1]['backupCount'] assert self.config.max_log_backups == backup_count class ColoredStreamHandlerTest(unittest.TestCase): """Tests for certbot._internal.log.ColoredStreamHandler""" def setUp(self): self.stream = io.StringIO() self.stream.isatty = lambda: True self.logger = logging.getLogger() self.logger.setLevel(logging.DEBUG) from certbot._internal.log import ColoredStreamHandler self.handler = ColoredStreamHandler(self.stream) self.logger.addHandler(self.handler) def tearDown(self): self.handler.close() def test_format(self): msg = 'I did a thing' self.logger.debug(msg) assert self.stream.getvalue() == '{0}\n'.format(msg) def test_format_and_red_level(self): msg = 'I did another thing' self.handler.red_level = logging.DEBUG self.logger.debug(msg) assert self.stream.getvalue() == \ '{0}{1}{2}\n'.format(util.ANSI_SGR_RED, msg, util.ANSI_SGR_RESET) class MemoryHandlerTest(unittest.TestCase): """Tests for certbot._internal.log.MemoryHandler""" def setUp(self): self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.DEBUG) self.msg = 'hi there' self.stream = io.StringIO() self.stream_handler = logging.StreamHandler(self.stream) from certbot._internal.log import MemoryHandler self.handler = MemoryHandler(self.stream_handler) self.logger.addHandler(self.handler) def tearDown(self): self.handler.close() self.stream_handler.close() def test_flush(self): self._test_log_debug() self.handler.flush(force=True) assert self.stream.getvalue() == self.msg + '\n' def test_not_flushed(self): # By default, logging.ERROR messages and higher are flushed self.logger.critical(self.msg) self.handler.flush() assert self.stream.getvalue() == '' def test_target_reset(self): self._test_log_debug() new_stream = io.StringIO() new_stream_handler = logging.StreamHandler(new_stream) self.handler.setTarget(new_stream_handler) self.handler.flush(force=True) assert self.stream.getvalue() == '' assert new_stream.getvalue() == self.msg + '\n' new_stream_handler.close() def _test_log_debug(self): self.logger.debug(self.msg) class TempHandlerTest(unittest.TestCase): """Tests for certbot._internal.log.TempHandler.""" def setUp(self): self.closed = False from certbot._internal.log import TempHandler self.handler = TempHandler() def tearDown(self): self.handler.close() def test_permissions(self): assert filesystem.check_permissions(self.handler.path, 0o600) def test_delete(self): self.handler.close() assert not os.path.exists(self.handler.path) def test_no_delete(self): self.handler.emit(mock.MagicMock()) self.handler.close() assert os.path.exists(self.handler.path) os.remove(self.handler.path) class PreArgParseExceptHookTest(unittest.TestCase): """Tests for certbot._internal.log.pre_arg_parse_except_hook.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.log import pre_arg_parse_except_hook return pre_arg_parse_except_hook(*args, **kwargs) @mock.patch('certbot._internal.log.post_arg_parse_except_hook') def test_it(self, mock_post_arg_parse_except_hook): memory_handler = mock.MagicMock() args = ('some', 'args',) kwargs = {'some': 'kwargs'} self._call(memory_handler, *args, **kwargs) mock_post_arg_parse_except_hook.assert_called_once_with( *args, **kwargs) memory_handler.flush.assert_called_once_with(force=True) class PostArgParseExceptHookTest(unittest.TestCase): """Tests for certbot._internal.log.post_arg_parse_except_hook.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.log import post_arg_parse_except_hook return post_arg_parse_except_hook(*args, **kwargs) def setUp(self): self.error_msg = 'test error message' self.log_path = 'foo.log' def test_base_exception(self): exc_type = BaseException mock_logger, output = self._test_common(exc_type, debug=False) self._assert_exception_logged(mock_logger.error, exc_type) self._assert_logfile_output(output) def test_debug(self): exc_type = ValueError mock_logger, output = self._test_common(exc_type, debug=True) self._assert_exception_logged(mock_logger.error, exc_type) self._assert_logfile_output(output) def test_quiet(self): exc_type = ValueError mock_logger, output = self._test_common(exc_type, debug=True, quiet=True) self._assert_exception_logged(mock_logger.error, exc_type) assert 'See the logfile' not in output def test_custom_error(self): exc_type = errors.PluginError mock_logger, output = self._test_common(exc_type, debug=False) self._assert_exception_logged(mock_logger.debug, exc_type) self._assert_quiet_output(mock_logger, output) def test_acme_error(self): # Get an arbitrary error code acme_code = next(iter(messages.ERROR_CODES)) def get_acme_error(msg): """Wraps ACME errors so the constructor takes only a msg.""" return messages.Error.with_code(acme_code, detail=msg) mock_logger, output = self._test_common(get_acme_error, debug=False) self._assert_exception_logged(mock_logger.debug, messages.Error) self._assert_quiet_output(mock_logger, output) assert messages.ERROR_PREFIX not in output def test_other_error(self): exc_type = ValueError mock_logger, output = self._test_common(exc_type, debug=False) self._assert_exception_logged(mock_logger.debug, exc_type) self._assert_quiet_output(mock_logger, output) def test_keyboardinterrupt(self): exc_type = KeyboardInterrupt mock_logger, output = self._test_common(exc_type, debug=False) mock_logger.error.assert_called_once_with('Exiting due to user request.') def _test_common(self, error_type, debug, quiet=False): """Returns the mocked logger and stderr output.""" mock_err = io.StringIO() def write_err(*args, **unused_kwargs): """Write error to mock_err.""" mock_err.write(args[0]) try: raise error_type(self.error_msg) except BaseException: exc_info = sys.exc_info() with mock.patch('certbot._internal.log.logger') as mock_logger: mock_logger.error.side_effect = write_err with mock.patch('certbot._internal.log.sys.stderr', mock_err): try: self._call( *exc_info, debug=debug, quiet=quiet, log_path=self.log_path) except SystemExit as exit_err: mock_err.write(str(exit_err)) else: # pragma: no cover self.fail('SystemExit not raised.') output = mock_err.getvalue() return mock_logger, output def _assert_exception_logged(self, log_func, exc_type): assert log_func.called call_kwargs = log_func.call_args[1] assert 'exc_info' in call_kwargs actual_exc_info = call_kwargs['exc_info'] expected_exc_info = (exc_type, mock.ANY, mock.ANY) assert actual_exc_info == expected_exc_info def _assert_logfile_output(self, output): assert 'See the logfile' in output assert self.log_path in output def _assert_quiet_output(self, mock_logger, output): assert mock_logger.exception.called is False assert mock_logger.debug.called assert self.error_msg in output class ExitWithAdviceTest(test_util.TempDirTestCase): """Tests for certbot._internal.log.exit_with_advice.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.log import exit_with_advice return exit_with_advice(*args, **kwargs) def test_log_file(self): log_file = os.path.join(self.tempdir, 'test.log') open(log_file, 'w').close() err_str = self._test_common(log_file) assert 'logfiles' not in err_str assert log_file in err_str def test_log_dir(self): err_str = self._test_common(self.tempdir) assert 'logfiles' in err_str assert self.tempdir in err_str # pylint: disable=inconsistent-return-statements def _test_common(self, *args, **kwargs): try: self._call(*args, **kwargs) except SystemExit as err: return str(err) self.fail('SystemExit was not raised.') # pragma: no cover if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/main_test.py0000664000175100017510000036171614561227515021322 0ustar00ericaerica# coding=utf-8 """Tests for certbot._internal.main.""" # pylint: disable=too-many-lines import contextlib import datetime from importlib import reload as reload_module import io import itertools import json import shutil import sys import tempfile import traceback from typing import List import unittest from unittest import mock import configobj import josepy as jose import pytest import pytz from acme.messages import Error as acme_error from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot import util from certbot._internal import account from certbot._internal import cli from certbot._internal import constants from certbot._internal import main from certbot._internal import updater from certbot._internal.plugins import disco from certbot._internal.plugins import manual from certbot._internal.plugins import null from certbot._internal.plugins import standalone from certbot.compat import filesystem from certbot.compat import os from certbot.plugins import enhancements import certbot.tests.util as test_util CERT_PATH = test_util.vector_path('cert_512.pem') CERT = test_util.vector_path('cert_512.pem') CSR = test_util.vector_path('csr_512.der') KEY = test_util.vector_path('rsa256_key.pem') JWK = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) RSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem') SS_CERT_PATH = test_util.vector_path('cert_2048.pem') class TestHandleCerts(unittest.TestCase): """Test for certbot._internal.main._handle_* methods""" @mock.patch("certbot._internal.main._handle_unexpected_key_type_migration") def test_handle_identical_cert_request_pending(self, mock_handle_migration): mock_lineage = mock.Mock() mock_lineage.ensure_deployed.return_value = False # pylint: disable=protected-access ret = main._handle_identical_cert_request(mock.Mock(), mock_lineage) assert ret == ("reinstall", mock_lineage) assert mock_handle_migration.called @mock.patch('certbot._internal.renewal.should_renew') @mock.patch("certbot.display.util.menu") @mock.patch("certbot._internal.main._handle_unexpected_key_type_migration") def test_handle_identical_cert_key_type_change(self, mock_handle_migration, mock_menu, mock_should_renew): mock_handle_migration.return_value = True mock_lineage = mock.Mock() mock_lineage.ensure_deployed.return_value = True mock_should_renew.return_value = False ret = main._handle_identical_cert_request(mock.MagicMock(verb="run", reinstall=False), mock_lineage) assert mock_handle_migration.called assert not mock_menu.called assert ret == ("renew", mock_lineage) @mock.patch("certbot._internal.main._handle_unexpected_key_type_migration") def test_handle_subset_cert_request(self, mock_handle_migration): mock_config = mock.Mock() mock_config.expand = True mock_lineage = mock.Mock() mock_lineage.names.return_value = ["dummy1", "dummy2"] ret = main._handle_subset_cert_request(mock_config, ["dummy1"], mock_lineage) assert ret == ("renew", mock_lineage) assert mock_handle_migration.called @mock.patch("certbot._internal.main.display_util.yesno") def test_handle_unexpected_key_type_migration(self, mock_yesno): config = mock.Mock() mock_set = mock.Mock() config.set_by_user = mock_set cert = mock.Mock() # If the key types do not differ, it should be a no-op. config.key_type = "rsa" cert.private_key_type = "rsa" main._handle_unexpected_key_type_migration(config, cert) mock_yesno.assert_not_called() assert config.key_type == cert.private_key_type # If the user confirms the change interactively, the key change should proceed silently. cert.private_key_type = "ecdsa" mock_yesno.return_value = True main._handle_unexpected_key_type_migration(config, cert) assert mock_set.call_count == 2 assert config.key_type == "rsa" # User does not interactively confirm the key type change. mock_yesno.return_value = False # If --key-type and --cert-name are both set, the key type change should proceed silently. mock_set.return_value = True main._handle_unexpected_key_type_migration(config, cert) assert config.key_type == "rsa" # If neither --key-type nor --cert-name are set, Certbot should keep the old key type. mock_set.return_value = False main._handle_unexpected_key_type_migration(config, cert) assert config.key_type == "ecdsa" # If --key-type is set and --cert-name isn't, Certbot should error. config.key_type = "rsa" mock_set.side_effect = lambda var: var != "certname" with pytest.raises(errors.Error, match="Please provide both --cert-name and --key-type"): main._handle_unexpected_key_type_migration(config, cert) # If --key-type is not set, Certbot should keep the old key type. mock_set.side_effect = lambda var: var != "key_type" main._handle_unexpected_key_type_migration(config, cert) assert config.key_type == "ecdsa" class RunTest(test_util.ConfigTestCase): """Tests for certbot._internal.main.run.""" def setUp(self): super().setUp() self.domain = 'example.org' patches = [ mock.patch('certbot._internal.main._get_and_save_cert'), mock.patch('certbot._internal.main.display_ops.success_installation'), mock.patch('certbot._internal.main.display_ops.success_renewal'), mock.patch('certbot._internal.main._init_le_client'), mock.patch('certbot._internal.main._suggest_donation_if_appropriate'), mock.patch('certbot._internal.main._report_new_cert'), mock.patch('certbot._internal.main._find_cert'), mock.patch('certbot._internal.eff.handle_subscription'), mock.patch('certbot._internal.main._report_next_steps') ] self.mock_auth = patches[0].start() self.mock_success_installation = patches[1].start() self.mock_success_renewal = patches[2].start() self.mock_init = patches[3].start() self.mock_suggest_donation = patches[4].start() self.mock_report_cert = patches[5].start() self.mock_find_cert = patches[6].start() self.mock_subscription = patches[7].start() self.mock_report_next_steps = patches[8].start() for patch in patches: self.addCleanup(patch.stop) def _call(self): args = '-a webroot -i null -d {0}'.format(self.domain).split() plugins = disco.PluginsRegistry.find_all() config = cli.prepare_and_parse_args(plugins, args) from certbot._internal.main import run run(config, plugins) def test_newcert_success(self): self.mock_auth.return_value = mock.Mock() self.mock_find_cert.return_value = True, None self._call() self.mock_success_installation.assert_called_once_with([self.domain]) self.mock_report_next_steps.assert_called_once_with(mock.ANY, None, mock.ANY, new_or_renewed_cert=True) def test_reinstall_success(self): self.mock_auth.return_value = mock.Mock() self.mock_find_cert.return_value = False, mock.Mock() self._call() self.mock_success_installation.assert_called_once_with([self.domain]) def test_renewal_success(self): self.mock_auth.return_value = mock.Mock() self.mock_find_cert.return_value = True, mock.Mock() self._call() self.mock_success_renewal.assert_called_once_with([self.domain]) @mock.patch('certbot._internal.main.plug_sel.choose_configurator_plugins') def test_run_enhancement_not_supported(self, mock_choose): mock_choose.return_value = (null.Installer(self.config, "null"), None) plugins = disco.PluginsRegistry.find_all() self.config.auto_hsts = True with pytest.raises(errors.NotSupportedError): main.run(self.config, plugins) @mock.patch('certbot._internal.main._install_cert') def test_cert_success_install_error(self, mock_install_cert): mock_install_cert.side_effect = errors.PluginError("Fake installation error") self.mock_auth.return_value = mock.Mock() self.mock_find_cert.return_value = True, None with pytest.raises(errors.PluginError): self._call() # Next steps should contain both renewal advice and installation error self.mock_report_next_steps.assert_called_once_with( mock.ANY, mock_install_cert.side_effect, mock.ANY, new_or_renewed_cert=True) # The final success message shouldn't be shown self.mock_success_installation.assert_not_called() @mock.patch('certbot._internal.main.plug_sel.choose_configurator_plugins') def test_run_must_staple_not_supported(self, mock_choose): mock_choose.return_value = (null.Installer(self.config, "null"), None) plugins = disco.PluginsRegistry.find_all() self.config.must_staple = True with pytest.raises(errors.NotSupportedError): main.run(self.config, plugins) class CertonlyTest(unittest.TestCase): """Tests for certbot._internal.main.certonly.""" def setUp(self): self.get_utility_patch = test_util.patch_display_util() self.mock_get_utility = self.get_utility_patch.start() def tearDown(self): self.get_utility_patch.stop() def _call(self, args): plugins = disco.PluginsRegistry.find_all() config = cli.prepare_and_parse_args(plugins, args) with mock.patch('certbot._internal.main._init_le_client') as mock_init: with mock.patch('certbot._internal.main._suggest_donation_if_appropriate'): with mock.patch('certbot._internal.eff.handle_subscription'): main.certonly(config, plugins) return mock_init() # returns the client @mock.patch('certbot._internal.main._find_cert') @mock.patch('certbot._internal.main._get_and_save_cert') @mock.patch('certbot._internal.main._report_new_cert') def test_no_reinstall_text_pause(self, unused_report, mock_auth, mock_find_cert): mock_notification = self.mock_get_utility().notification mock_notification.side_effect = self._assert_no_pause mock_auth.return_value = mock.Mock() mock_find_cert.return_value = False, None self._call('certonly --webroot -d example.com'.split()) def _assert_no_pause(self, *args, **kwargs): # pylint: disable=unused-argument assert kwargs.get("pause") is False @mock.patch('certbot._internal.main._report_next_steps') @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.cert_manager.domains_for_certname') @mock.patch('certbot._internal.renewal.renew_cert') @mock.patch('certbot._internal.main._handle_unexpected_key_type_migration') @mock.patch('certbot._internal.main._report_new_cert') def test_find_lineage_for_domains_and_certname(self, mock_report_cert, mock_handle_type, mock_renew_cert, mock_domains, mock_lineage, mock_report_next_steps): domains = ['example.com', 'test.org'] mock_domains.return_value = domains mock_lineage.names.return_value = domains self._call(('certonly --webroot -d example.com -d test.org ' '--cert-name example.com').split()) assert mock_lineage.call_count == 1 assert mock_domains.call_count == 1 assert mock_renew_cert.call_count == 1 assert mock_report_cert.call_count == 1 assert mock_handle_type.call_count == 1 mock_report_next_steps.assert_called_once_with( mock.ANY, None, mock.ANY, new_or_renewed_cert=True) # user confirms updating lineage with new domains self._call(('certonly --webroot -d example.com -d test.com ' '--cert-name example.com').split()) assert mock_lineage.call_count == 2 assert mock_domains.call_count == 2 assert mock_renew_cert.call_count == 2 assert mock_report_cert.call_count == 2 assert mock_handle_type.call_count == 2 # error in _ask_user_to_confirm_new_names self.mock_get_utility().yesno.return_value = False with pytest.raises(errors.ConfigurationError): self._call('certonly --webroot -d example.com -d test.com --cert-name example.com'.split()) @mock.patch('certbot._internal.main._report_next_steps') @mock.patch('certbot._internal.cert_manager.domains_for_certname') @mock.patch('certbot.display.ops.choose_names') @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.main._report_new_cert') def test_find_lineage_for_domains_new_certname(self, mock_report_cert, mock_lineage, mock_choose_names, mock_domains_for_certname, unused_mock_report_next_steps): mock_lineage.return_value = None # no lineage with this name but we specified domains so create a new cert self._call(('certonly --webroot -d example.com -d test.com ' '--cert-name example.com').split()) assert mock_lineage.call_count == 1 assert mock_report_cert.call_count == 1 # no lineage with this name and we didn't give domains mock_choose_names.return_value = ["somename"] mock_domains_for_certname.return_value = None self._call(('certonly --webroot --cert-name example.com').split()) assert mock_choose_names.called is True @mock.patch('certbot._internal.main._report_next_steps') @mock.patch('certbot._internal.main._get_and_save_cert') @mock.patch('certbot._internal.main._csr_get_and_save_cert') @mock.patch('certbot._internal.cert_manager.lineage_for_certname') def test_dryrun_next_steps_no_cert_saved(self, mock_lineage, mock_csr_get_cert, unused_mock_get_cert, mock_report_next_steps): """certonly --dry-run shouldn't report creation of a certificate in NEXT STEPS.""" mock_lineage.return_value = None mock_csr_get_cert.return_value = ("/cert", "/chain", "/fullchain") for flag in (f"--csr {CSR}", "-d example.com"): self._call(f"certonly {flag} --webroot --cert-name example.com --dry-run".split()) mock_report_next_steps.assert_called_once_with( mock.ANY, mock.ANY, mock.ANY, new_or_renewed_cert=False) mock_report_next_steps.reset_mock() @mock.patch('certbot._internal.main._report_next_steps') @mock.patch('certbot._internal.main._report_new_cert') @mock.patch('certbot._internal.main._find_cert') @mock.patch('certbot._internal.main._get_and_save_cert') @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins') def test_installer_runs_restart(self, mock_sel, mock_get_cert, mock_find_cert, unused_report_new, unused_report_next): mock_installer = mock.MagicMock() mock_sel.return_value = (mock_installer, None) mock_get_cert.return_value = mock.MagicMock() mock_find_cert.return_value = (True, None) self._call('certonly --nginx -d example.com'.split()) mock_installer.restart.assert_called_once() @mock.patch('certbot._internal.main._report_next_steps') @mock.patch('certbot._internal.main._report_new_cert') @mock.patch('certbot._internal.main._find_cert') @mock.patch('certbot._internal.main._get_and_save_cert') @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins') def test_dryrun_installer_doesnt_restart(self, mock_sel, mock_get_cert, mock_find_cert, unused_report_new, unused_report_next): mock_installer = mock.MagicMock() mock_sel.return_value = (mock_installer, None) mock_get_cert.return_value = mock.MagicMock() mock_find_cert.return_value = (True, None) self._call('certonly --nginx -d example.com --dry-run'.split()) mock_installer.restart.assert_not_called() @mock.patch('certbot._internal.main._report_next_steps') @mock.patch('certbot._internal.main._report_new_cert') @mock.patch('certbot._internal.main._find_cert') @mock.patch('certbot._internal.main._get_and_save_cert') def test_invalid_installer(self, mock_get_cert, mock_find_cert, unused_report_new, unused_report_next): mock_get_cert.return_value = mock.MagicMock() mock_find_cert.return_value = (True, None) self._call((f'certonly --webroot -w {tempfile.gettempdir()} ' + '-i standalone -d example.com').split()) class FindDomainsOrCertnameTest(unittest.TestCase): """Tests for certbot._internal.main._find_domains_or_certname.""" @mock.patch('certbot.display.ops.choose_names') def test_display_ops(self, mock_choose_names): mock_config = mock.Mock(domains=None, certname=None) mock_choose_names.return_value = "domainname" # pylint: disable=protected-access assert main._find_domains_or_certname(mock_config, None) == ("domainname", None) @mock.patch('certbot.display.ops.choose_names') def test_no_results(self, mock_choose_names): mock_config = mock.Mock(domains=None, certname=None) mock_choose_names.return_value = [] # pylint: disable=protected-access with pytest.raises(errors.Error): main._find_domains_or_certname(mock_config, None) @mock.patch('certbot._internal.cert_manager.domains_for_certname') def test_grab_domains(self, mock_domains): mock_config = mock.Mock(domains=None, certname="one.com") mock_domains.return_value = ["one.com", "two.com"] # pylint: disable=protected-access assert main._find_domains_or_certname(mock_config, None) == \ (["one.com", "two.com"], "one.com") class RevokeTest(test_util.TempDirTestCase): """Tests for certbot._internal.main.revoke.""" def setUp(self): super().setUp() shutil.copy(CERT_PATH, self.tempdir) self.tmp_cert_path = os.path.abspath(os.path.join(self.tempdir, 'cert_512.pem')) patches = [ mock.patch('certbot._internal.client.acme_client'), mock.patch('certbot._internal.client.Client'), mock.patch('certbot._internal.main._determine_account'), mock.patch('certbot._internal.main.display_ops.success_revocation') ] self.mock_acme_client = patches[0].start().ClientV2 patches[1].start() self.mock_determine_account = patches[2].start() self.mock_success_revoke = patches[3].start() for patch in patches: self.addCleanup(patch.stop) from certbot._internal.account import Account self.regr = mock.MagicMock() self.meta = Account.Meta( creation_host="test.certbot.org", creation_dt=datetime.datetime( 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC)) self.acc = Account(self.regr, JWK, self.meta) self.mock_determine_account.return_value = (self.acc, None) def _call(self, args=None): if not args: args = 'revoke --cert-path={0} ' args = args.format(self.tmp_cert_path).split() plugins = disco.PluginsRegistry.find_all() config = cli.prepare_and_parse_args(plugins, args) from certbot._internal.main import revoke revoke(config, plugins) @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.main.client.acme_client') def test_revoke_with_reason(self, mock_acme_client, mock_delete_if_appropriate): mock_delete_if_appropriate.return_value = False mock_revoke = mock_acme_client.ClientV2().revoke expected = [] for reason, code in constants.REVOCATION_REASONS.items(): args = 'revoke --cert-path={0} --reason {1}'.format(self.tmp_cert_path, reason).split() self._call(args) expected.append(mock.call(mock.ANY, code)) args = 'revoke --cert-path={0} --reason {1}'.format(self.tmp_cert_path, reason.upper()).split() self._call(args) expected.append(mock.call(mock.ANY, code)) assert expected == mock_revoke.call_args_list @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.storage.RenewableCert') @mock.patch('certbot._internal.storage.renewal_file_for_certname') @mock.patch('certbot._internal.client.acme_from_config_key') def test_revoke_by_certname(self, mock_acme_from_config, unused_mock_renewal_file_for_certname, mock_cert, mock_delete_if_appropriate): mock_acme_from_config.return_value = self.mock_acme_client mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server="https://acme.example") args = 'revoke --cert-name=example.com'.split() mock_delete_if_appropriate.return_value = False self._call(args) assert mock_acme_from_config.call_args_list[0][0][0].server == \ 'https://acme.example' self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.storage.RenewableCert') @mock.patch('certbot._internal.storage.renewal_file_for_certname') @mock.patch('certbot._internal.client.acme_from_config_key') def test_revoke_by_certname_and_server(self, mock_acme_from_config, unused_mock_renewal_file_for_certname, mock_cert, mock_delete_if_appropriate): """Revoking with --server should use the server from the CLI""" mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server="https://acme.example") args = 'revoke --cert-name=example.com --server https://other.example'.split() mock_delete_if_appropriate.return_value = False self._call(args) assert mock_acme_from_config.call_args_list[0][0][0].server == \ 'https://other.example' self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.storage.RenewableCert') @mock.patch('certbot._internal.storage.renewal_file_for_certname') @mock.patch('certbot._internal.client.acme_from_config_key') def test_revoke_by_certname_empty_server(self, mock_acme_from_config, unused_mock_renewal_file_for_certname, mock_cert, mock_delete_if_appropriate): """Revoking with --cert-name where the lineage server is empty shouldn't crash """ mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server=None) args = 'revoke --cert-name=example.com'.split() mock_delete_if_appropriate.return_value = False self._call(args) assert mock_acme_from_config.call_args_list[0][0][0].server == \ constants.CLI_DEFAULTS['server'] self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) @mock.patch('certbot._internal.main._delete_if_appropriate') def test_revocation_success(self, mock_delete_if_appropriate): self._call() mock_delete_if_appropriate.return_value = False self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) def test_revocation_error(self): from acme import errors as acme_errors self.mock_acme_client.side_effect = acme_errors.ClientError() with pytest.raises(acme_errors.ClientError): self._call() self.mock_success_revoke.assert_not_called() @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.cert_manager.delete') @test_util.patch_display_util() def test_revocation_with_prompt(self, mock_get_utility, mock_delete, mock_delete_if_appropriate): mock_get_utility().yesno.return_value = False mock_delete_if_appropriate.return_value = False self._call() assert mock_delete.called is False class ReconfigureTest(test_util.TempDirTestCase): """Tests for certbot._internal.main.reconfigure""" def setUp(self): super().setUp() self.get_utility_patch = test_util.patch_display_util() self.mock_get_utility = self.get_utility_patch.start() self.patchers = { 'check_symlinks': mock.patch('certbot._internal.storage.RenewableCert._check_symlinks'), 'cert_names': mock.patch('certbot._internal.storage.RenewableCert.names'), 'pick_installer': mock.patch('certbot._internal.plugins.selection.pick_installer'), 'pick_auth': mock.patch('certbot._internal.plugins.selection.pick_authenticator'), 'find_init': mock.patch('certbot._internal.plugins.disco.PluginsRegistry.find_init'), '_get_and_save_cert': mock.patch('certbot._internal.main._get_and_save_cert'), '_init_le_client': mock.patch('certbot._internal.main._init_le_client'), 'list_hooks': mock.patch('certbot._internal.hooks.list_hooks'), } self.mocks = {k: v.start() for k, v in self.patchers.items()} self.mocks['cert_names'].return_value = ['example.com'] self.config_dir = os.path.join(self.tempdir, 'config') renewal_configs_dir = os.path.join(self.config_dir, 'renewal') if not os.path.exists(renewal_configs_dir): filesystem.makedirs(renewal_configs_dir) self.renewal_file = os.path.join(renewal_configs_dir, 'example.com.conf') original_config = """ version = 1.32.0 archive_dir = /etc/letsencrypt/archive/example.com cert = /etc/letsencrypt/live/example.com/cert.pem privkey = /etc/letsencrypt/live/example.com/privkey.pem chain = /etc/letsencrypt/live/example.com/chain.pem fullchain = /etc/letsencrypt/live/example.com/fullchain.pem # Options used in the renewal process [renewalparams] account = ee43634db0aa4e6804f152be39990e6a server = https://acme-v02.api.letsencrypt.org/directory authenticator = nginx installer = nginx key_type = rsa """ with open(self.renewal_file, 'w') as f: f.write(original_config) with open(self.renewal_file, 'r') as f: self.original_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8') def tearDown(self): super().tearDown() self.get_utility_patch.stop() for patch in self.patchers.values(): patch.stop() def _call(self, passed_args): full_args = passed_args + ['--config-dir', self.config_dir] plugins = disco.PluginsRegistry.find_all() config = cli.prepare_and_parse_args(plugins, full_args) from certbot._internal.main import reconfigure reconfigure(config, plugins) with open(self.renewal_file, 'r') as f: updated_conf = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8') return updated_conf def test_domains_set(self): with pytest.raises(errors.ConfigurationError): self._call('--cert-name cert1 -d one.cert.com'.split()) @mock.patch('certbot._internal.cert_manager.get_certnames') def test_asks_for_certname(self, mock_cert_manager): named_mock = mock.Mock() named_mock.name = 'nginx' self.mocks['pick_installer'].return_value = named_mock self.mocks['pick_auth'].return_value = named_mock self.mocks['find_init'].return_value = named_mock mock_cert_manager.return_value = ['example.com'] self._call('--nginx'.split()) assert mock_cert_manager.call_count == 1 def test_update_configurator(self): named_mock = mock.Mock() named_mock.name = 'apache' self.mocks['pick_installer'].return_value = named_mock self.mocks['pick_auth'].return_value = named_mock self.mocks['find_init'].return_value = named_mock new_config = self._call('--cert-name example.com --apache'.split()) assert new_config['renewalparams']['authenticator'] == 'apache' def test_only_intended_changes(self): """ Check that we don't accidentally modify anything that we didn't mean to """ named_mock = mock.Mock() named_mock.name = 'apache' self.mocks['pick_installer'].return_value = named_mock self.mocks['pick_auth'].return_value = named_mock self.mocks['find_init'].return_value = named_mock new_config = self._call('--cert-name example.com --apache'.split()) # Undo the changes we made in calling and in testing new_config['renewalparams']['authenticator'] = 'nginx' new_config['renewalparams']['installer'] = 'nginx' del new_config['renewalparams']['config_dir'] new_config['version'] = self.original_config['version'] assert new_config == self.original_config @mock.patch('certbot._internal.hooks.validate_hooks') def test_staging_used(self, unused_validate_hooks): """ Check that we use the staging server for the dry run """ assert self.original_config['renewalparams']['server'] == \ 'https://acme-v02.api.letsencrypt.org/directory' self._call('--cert-name example.com --pre-hook'.split() + ['echo pre']) assert 'staging' in self.mocks['_init_le_client'].call_args.args[0].server assert 'staging' in self.mocks['_get_and_save_cert'].call_args.args[1].server def test_new_account_or_server_errors(self): """ Check that we error when attempting to change the account id or server, but not when it's the same """ orig_account_id = self.original_config['renewalparams']['account'] orig_server = self.original_config['renewalparams']['server'] # new account try: self._call(f'--cert-name example.com --account newaccountid'.split()) except errors.ConfigurationError as err: assert "Using reconfigure to change the ACME account" in str(err) # check that config isn't modified with open(self.renewal_file, 'r') as f: new_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8') assert new_config['renewalparams']['account'] == orig_account_id # same account new_config = self._call(f'--cert-name example.com --account {orig_account_id}'.split()) assert new_config['renewalparams']['account'] == orig_account_id # new server try: self._call(f'--cert-name example.com --server x.com'.split()) except errors.ConfigurationError as err: assert "Using reconfigure to change the ACME account" in str(err) # check that config isn't modified with open(self.renewal_file, 'r') as f: new_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8') assert new_config['renewalparams']['server'] == orig_server # same server new_config = self._call(f'--cert-name example.com --server {orig_server}'.split()) assert new_config['renewalparams']['server'] == orig_server @mock.patch('certbot._internal.hooks.validate_hooks') def test_update_hooks(self, unused_validate_hooks): assert 'pre_hook' not in self.original_config # test set new_config = self._call('--cert-name example.com --pre-hook'.split() + ['echo pre']) assert new_config['renewalparams']['pre_hook'] == 'echo pre' # test update new_config = self._call('--cert-name example.com --pre-hook'.split() + ['echo pre2']) assert new_config['renewalparams']['pre_hook'] == 'echo pre2' # test deploy hook is set even though we did a dry run assert 'renew_hook' not in self.original_config new_config = self._call('--cert-name example.com --deploy-hook'.split() + ['echo deploy']) assert new_config['renewalparams']['renew_hook'] == 'echo deploy' def test_dry_run_fails(self): # set side effect of raising error self.mocks['_get_and_save_cert'].side_effect = errors.Error try: self._call('--cert-name example.com --apache'.split()) except errors.Error: pass # check that config isn't modified with open(self.renewal_file, 'r') as f: new_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8') assert new_config['renewalparams']['authenticator'] == 'nginx' @mock.patch('certbot._internal.main.display_util.notify') def test_report_results(self, mock_notify): # make sure report results works when config has a webroot map original_config = """ version = 2.0.0 archive_dir = /etc/letsencrypt/archive/example.com cert = /etc/letsencrypt/live/example.com/cert.pem privkey = /etc/letsencrypt/live/example.com/privkey.pem chain = /etc/letsencrypt/live/example.com/chain.pem fullchain = /etc/letsencrypt/live/example.com/fullchain.pem # Options used in the renewal process [renewalparams] account = ee43634db0aa4e6804f152be39990e6a server = https://acme-staging-v02.api.letsencrypt.org/directory authenticator = webroot installer = nginx key_type = ecdsa webroot_path = /var/www/html, [[webroot_map]] example.com = /var/www/html """ with open(self.renewal_file, 'w') as f: f.write(original_config) with open(self.renewal_file, 'r') as f: self.original_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8') named_mock = mock.Mock() named_mock.name = 'nginx' self.mocks['pick_auth'].return_value = named_mock self.mocks['find_init'].return_value = named_mock new_config = self._call('--cert-name example.com --nginx'.split()) assert new_config['renewalparams']['authenticator'] == 'nginx' mock_notify.assert_called_with( '\nSuccessfully updated configuration.'+ '\nChanges will apply when the certificate renews.') class DeleteIfAppropriateTest(test_util.ConfigTestCase): """Tests for certbot._internal.main._delete_if_appropriate """ def _call(self, mock_config): from certbot._internal.main import _delete_if_appropriate _delete_if_appropriate(mock_config) def _test_delete_opt_out_common(self): with mock.patch('certbot._internal.cert_manager.delete') as mock_delete: self._call(self.config) mock_delete.assert_not_called() @test_util.patch_display_util() def test_delete_flag_opt_out(self, unused_mock_get_utility): self.config.delete_after_revoke = False self._test_delete_opt_out_common() @test_util.patch_display_util() def test_delete_prompt_opt_out(self, mock_get_utility): util_mock = mock_get_utility() util_mock.yesno.return_value = False self._test_delete_opt_out_common() @mock.patch("certbot._internal.main.logger.warning") @mock.patch('certbot._internal.storage.renewal_file_for_certname') @mock.patch('certbot._internal.cert_manager.delete') @mock.patch('certbot._internal.cert_manager.match_and_check_overlaps') @mock.patch('certbot._internal.storage.full_archive_path') @mock.patch('certbot._internal.cert_manager.cert_path_to_lineage') @test_util.patch_display_util() def test_overlapping_archive_dirs(self, mock_get_utility, mock_cert_path_to_lineage, mock_archive, mock_match_and_check_overlaps, mock_delete, mock_renewal_file_for_certname, mock_warning): # pylint: disable = unused-argument config = self.config config.cert_path = "/some/reasonable/path" config.certname = "" mock_cert_path_to_lineage.return_value = "example.com" mock_match_and_check_overlaps.side_effect = errors.OverlappingMatchFound() self._call(config) mock_delete.assert_not_called() assert mock_warning.call_count == 1 @mock.patch('certbot._internal.storage.renewal_file_for_certname') @mock.patch('certbot._internal.cert_manager.match_and_check_overlaps') @mock.patch('certbot._internal.storage.full_archive_path') @mock.patch('certbot._internal.cert_manager.delete') @mock.patch('certbot._internal.cert_manager.cert_path_to_lineage') @test_util.patch_display_util() def test_cert_path_only(self, mock_get_utility, mock_cert_path_to_lineage, mock_delete, mock_archive, mock_overlapping_archive_dirs, mock_renewal_file_for_certname): # pylint: disable = unused-argument config = self.config config.cert_path = "/some/reasonable/path" config.certname = "" mock_cert_path_to_lineage.return_value = "example.com" mock_overlapping_archive_dirs.return_value = False self._call(config) assert mock_delete.call_count == 1 @mock.patch('certbot._internal.storage.renewal_file_for_certname') @mock.patch('certbot._internal.cert_manager.match_and_check_overlaps') @mock.patch('certbot._internal.storage.full_archive_path') @mock.patch('certbot._internal.cert_manager.cert_path_to_lineage') @mock.patch('certbot._internal.cert_manager.delete') @test_util.patch_display_util() def test_noninteractive_deletion(self, mock_get_utility, mock_delete, mock_cert_path_to_lineage, mock_full_archive_dir, mock_match_and_check_overlaps, mock_renewal_file_for_certname): # pylint: disable = unused-argument config = self.config config.noninteractive_mode = True config.cert_path = "/some/reasonable/path" config.certname = "" mock_cert_path_to_lineage.return_value = "example.com" mock_full_archive_dir.return_value = "" mock_match_and_check_overlaps.return_value = "" self._call(config) assert mock_delete.call_count == 1 @mock.patch('certbot._internal.storage.renewal_file_for_certname') @mock.patch('certbot._internal.cert_manager.match_and_check_overlaps') @mock.patch('certbot._internal.storage.full_archive_path') @mock.patch('certbot._internal.cert_manager.cert_path_to_lineage') @mock.patch('certbot._internal.cert_manager.delete') @test_util.patch_display_util() def test_opt_in_deletion(self, mock_get_utility, mock_delete, mock_cert_path_to_lineage, mock_full_archive_dir, mock_match_and_check_overlaps, mock_renewal_file_for_certname): config = self.config config.delete_after_revoke = True config.cert_path = "/some/reasonable/path" config.certname = "" mock_cert_path_to_lineage.return_value = "example.com" mock_full_archive_dir.return_value = "" mock_match_and_check_overlaps.return_value = "" self._call(config) assert mock_delete.call_count == 1 assert not mock_get_utility().yesno.called class DetermineAccountTest(test_util.ConfigTestCase): """Tests for certbot._internal.main._determine_account.""" def setUp(self): super().setUp() self.config.account = None self.config.email = None self.config.register_unsafely_without_email = False self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')] self.account_storage = account.AccountMemoryStorage() # For use in saving accounts: fake out the new_authz URL. self.mock_client = mock.MagicMock() self.mock_client.directory.new_authz = "hi" def _call(self): # pylint: disable=protected-access from certbot._internal.main import _determine_account with mock.patch('certbot._internal.main.account.AccountFileStorage') as mock_storage, \ test_util.patch_display_util(): mock_storage.return_value = self.account_storage return _determine_account(self.config) @mock.patch('certbot._internal.client.register') @mock.patch('certbot._internal.client.display_ops.get_email') def _register_error_common(self, err_msg, exception, mock_get_email, mock_register): mock_get_email.return_value = 'foo@bar.baz' mock_register.side_effect = exception try: self._call() except errors.Error as err: assert f"Unable to register an account with ACME server. {err_msg}" == \ str(err) def test_args_account_set(self): self.account_storage.save(self.accs[1], self.mock_client) self.config.account = self.accs[1].id assert (self.accs[1], None) == self._call() assert self.accs[1].id == self.config.account assert self.config.email is None def test_single_account(self): self.account_storage.save(self.accs[0], self.mock_client) assert (self.accs[0], None) == self._call() assert self.accs[0].id == self.config.account assert self.config.email is None @mock.patch('certbot._internal.client.display_ops.choose_account') def test_multiple_accounts(self, mock_choose_accounts): for acc in self.accs: self.account_storage.save(acc, self.mock_client) mock_choose_accounts.return_value = self.accs[1] assert (self.accs[1], None) == self._call() assert set(mock_choose_accounts.call_args[0][0]) == set(self.accs) assert self.accs[1].id == self.config.account assert self.config.email is None @mock.patch('certbot._internal.client.display_ops.choose_account') def test_multiple_accounts_canceled(self, mock_choose_accounts): for acc in self.accs: self.account_storage.save(acc, self.mock_client) mock_choose_accounts.return_value = None try: self._call() except errors.Error as err: assert "No account has been chosen" in str(err) @mock.patch('certbot._internal.client.display_ops.get_email') @mock.patch('certbot._internal.main.display_util.notify') def test_no_accounts_no_email(self, mock_notify, mock_get_email): mock_get_email.return_value = 'foo@bar.baz' with mock.patch('certbot._internal.main.client') as client: client.register.return_value = ( self.accs[0], mock.sentinel.acme) assert (self.accs[0], mock.sentinel.acme) == self._call() client.register.assert_called_once_with( self.config, self.account_storage, tos_cb=mock.ANY) assert self.accs[0].id == self.config.account assert 'foo@bar.baz' == self.config.email mock_notify.assert_called_once_with('Account registered.') def test_no_accounts_email(self): self.config.email = 'other email' with mock.patch('certbot._internal.main.client') as client: client.register.return_value = (self.accs[1], mock.sentinel.acme) self._call() assert self.accs[1].id == self.config.account assert 'other email' == self.config.email def test_register_error_certbot(self): err_msg = "Some error message raised by Certbot" self._register_error_common(err_msg, errors.Error(err_msg)) def test_register_error_acme_type_and_detail(self): err_msg = ("Error returned by the ACME server: must agree to terms of service") exception = acme_error(typ = "urn:ietf:params:acme:error:malformed", detail = "must agree to terms of service") self._register_error_common(err_msg, exception) def test_register_error_acme_type_only(self): err_msg = ("Error returned by the ACME server: The server experienced an internal error") exception = acme_error(typ = "urn:ietf:params:acme:error:serverInternal") self._register_error_common(err_msg, exception) class MainTest(test_util.ConfigTestCase): """Tests for different commands.""" def setUp(self): super().setUp() filesystem.mkdir(self.config.logs_dir) self.standard_args = ['--config-dir', self.config.config_dir, '--work-dir', self.config.work_dir, '--logs-dir', self.config.logs_dir, '--text'] self.mock_sleep = mock.patch('time.sleep').start() def tearDown(self): # Reset globals in cli reload_module(cli) super().tearDown() def _call(self, args, stdout=None, mockisfile=False): """Run the cli with output streams, actual client and optionally os.path.isfile() mocked out""" if mockisfile: orig_open = os.path.isfile def mock_isfile(fn, *args, **kwargs): # pylint: disable=unused-argument """Mock os.path.isfile()""" if (fn.endswith("cert") or fn.endswith("chain") or fn.endswith("privkey")): return True return orig_open(fn) with mock.patch("certbot.compat.os.path.isfile") as mock_if: mock_if.side_effect = mock_isfile with mock.patch('certbot._internal.main.client') as client: ret, stdout, stderr = self._call_no_clientmock(args, stdout) return ret, stdout, stderr, client else: with mock.patch('certbot._internal.main.client') as client: ret, stdout, stderr = self._call_no_clientmock(args, stdout) return ret, stdout, stderr, client def _call_no_clientmock(self, args, stdout=None): """Run the client with output streams mocked out""" args = self.standard_args + args toy_stdout = stdout if stdout else io.StringIO() with mock.patch('certbot._internal.main.sys.stdout', new=toy_stdout): with mock.patch('certbot._internal.main.sys.stderr') as stderr: with mock.patch("certbot.util.atexit"): ret = main.main(args[:]) # NOTE: parser can alter its args! return ret, toy_stdout, stderr def test_no_flags(self): with mock.patch('certbot._internal.main.run') as mock_run: self._call([]) assert 1 == mock_run.call_count def test_version_string_program_name(self): toy_out = io.StringIO() toy_err = io.StringIO() with mock.patch('certbot._internal.main.sys.stdout', new=toy_out): with mock.patch('certbot._internal.main.sys.stderr', new=toy_err): try: main.main(["--version"]) except SystemExit: pass finally: output = toy_out.getvalue() or toy_err.getvalue() assert "certbot" in output, "Output is {0}".format(output) def _cli_missing_flag(self, args, message): "Ensure that a particular error raises a missing cli flag error containing message" exc = None try: with mock.patch('certbot._internal.main.sys.stderr'): main.main(self.standard_args + args[:]) # NOTE: parser can alter its args! except errors.MissingCommandlineFlag as exc_: exc = exc_ assert message in str(exc) assert exc is not None @mock.patch('certbot._internal.log.post_arg_parse_setup') def test_noninteractive(self, _): args = ['-n', 'certonly'] self._cli_missing_flag(args, "specify a plugin") args.extend(['--standalone', '-d', 'eg.is']) self._cli_missing_flag(args, "register before running") @mock.patch('certbot._internal.eff.handle_subscription') @mock.patch('certbot._internal.log.post_arg_parse_setup') @mock.patch('certbot._internal.main._report_new_cert') @mock.patch('certbot._internal.main._determine_account') @mock.patch('certbot._internal.main.client.Client.obtain_and_enroll_certificate') @mock.patch('certbot._internal.main._get_and_save_cert') def test_user_agent(self, gsc, _obt, det, _, __, ___): # Normally the client is totally mocked out, but here we need more # arguments to automate it... args = ["--standalone", "certonly", "-m", "none@none.com", "-d", "example.com", '--agree-tos'] + self.standard_args det.return_value = mock.MagicMock(), None gsc.return_value = mock.MagicMock() with mock.patch('certbot._internal.main.client.acme_client') as acme_client: acme_net = acme_client.ClientNetwork self._call_no_clientmock(args) os_ver = util.get_os_info_ua() ua = acme_net.call_args[1]["user_agent"] assert os_ver in ua import platform plat = platform.platform() if "linux" in plat.lower(): assert util.get_os_info_ua() in ua with mock.patch('certbot._internal.main.client.acme_client') as acme_client: acme_net = acme_client.ClientNetwork ua = "bandersnatch" args += ["--user-agent", ua] self._call_no_clientmock(args) acme_net.assert_called_once_with(mock.ANY, account=mock.ANY, verify_ssl=True, user_agent=ua, alg=jose.RS256) @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @mock.patch('certbot._internal.main.plug_sel.pick_installer') def test_installer_selection(self, mock_pick_installer, _rec): self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert', '--key-path', 'privkey', '--chain-path', 'chain'], mockisfile=True) assert mock_pick_installer.call_count == 1 @mock.patch('certbot._internal.main._install_cert') @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @mock.patch('certbot._internal.main.plug_sel.pick_installer') def test_installer_certname(self, _inst, _rec, mock_install): mock_lineage = mock.MagicMock(cert_path=test_util.temp_join('cert'), chain_path=test_util.temp_join('chain'), fullchain_path=test_util.temp_join('chain'), key_path=test_util.temp_join('privkey')) with mock.patch("certbot._internal.cert_manager.lineage_for_certname") as mock_getlin: mock_getlin.return_value = mock_lineage self._call(['install', '--cert-name', 'whatever'], mockisfile=True) call_config = mock_install.call_args[0][0] assert call_config.cert_path == test_util.temp_join('cert') assert call_config.fullchain_path == test_util.temp_join('chain') assert call_config.key_path == test_util.temp_join('privkey') @mock.patch('certbot._internal.log.post_arg_parse_setup') @mock.patch('certbot._internal.main._install_cert') @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @mock.patch('certbot._internal.main.plug_sel.pick_installer') def test_installer_param_override(self, _inst, _rec, mock_install, _): mock_lineage = mock.MagicMock(cert_path=test_util.temp_join('cert'), chain_path=test_util.temp_join('chain'), fullchain_path=test_util.temp_join('chain'), key_path=test_util.temp_join('privkey')) with mock.patch("certbot._internal.cert_manager.lineage_for_certname") as mock_getlin: mock_getlin.return_value = mock_lineage self._call(['install', '--cert-name', 'whatever', '--key-path', test_util.temp_join('overriding_privkey')], mockisfile=True) call_config = mock_install.call_args[0][0] assert call_config.cert_path == test_util.temp_join('cert') assert call_config.fullchain_path == test_util.temp_join('chain') assert call_config.chain_path == test_util.temp_join('chain') assert call_config.key_path == test_util.temp_join('overriding_privkey') mock_install.reset() self._call(['install', '--cert-name', 'whatever', '--cert-path', test_util.temp_join('overriding_cert')], mockisfile=True) call_config = mock_install.call_args[0][0] assert call_config.cert_path == test_util.temp_join('overriding_cert') assert call_config.fullchain_path == test_util.temp_join('chain') assert call_config.key_path == test_util.temp_join('privkey') @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @mock.patch('certbot._internal.main.plug_sel.pick_installer') def test_installer_param_error(self, _inst, _rec): with pytest.raises(errors.ConfigurationError): self._call(['install', '--cert-name', 'notfound', '--key-path', 'invalid']) @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @mock.patch('certbot._internal.main.plug_sel.pick_installer') @mock.patch('certbot._internal.cert_manager.get_certnames') @mock.patch('certbot._internal.main._install_cert') def test_installer_select_cert(self, mock_inst, mock_getcert, _inst, _rec): mock_lineage = mock.MagicMock(cert_path=test_util.temp_join('cert'), chain_path=test_util.temp_join('chain'), fullchain_path=test_util.temp_join('chain'), key_path=test_util.temp_join('privkey')) with mock.patch("certbot._internal.cert_manager.lineage_for_certname") as mock_getlin: mock_getlin.return_value = mock_lineage self._call(['install'], mockisfile=True) assert mock_getcert.called assert mock_inst.called @mock.patch('certbot._internal.eff.handle_subscription') @mock.patch('certbot._internal.log.post_arg_parse_setup') @mock.patch('certbot._internal.main._report_new_cert') @mock.patch('certbot.util.exe_exists') def test_configurator_selection(self, mock_exe_exists, _, __, ___): mock_exe_exists.return_value = True real_plugins = disco.PluginsRegistry.find_all() args = ['--apache', '--authenticator', 'standalone'] # This needed two calls to find_all(), which we're avoiding for now # because of possible side effects: # https://github.com/certbot/certbot/commit/51ed2b681f87b1eb29088dd48718a54f401e4855 # with mock.patch('certbot._internal.cli.plugins_testable') as plugins: # plugins.return_value = {"apache": True, "nginx": True} # ret, _, _, _ = self._call(args) # self.assertTrue("Too many flags setting" in ret) args = ["install", "--nginx", "--cert-path", test_util.temp_join('blah'), "--key-path", test_util.temp_join('blah'), "--nginx-server-root", "/nonexistent/thing", "-d", "example.com", "--debug"] if "nginx" in real_plugins: # Sending nginx a non-existent conf dir will simulate misconfiguration # (we can only do that if certbot-nginx is actually present) ret, _, _, _ = self._call(args) assert "The nginx plugin is not working" in ret assert "MisconfigurationError" in ret self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") with mock.patch("certbot._internal.main._init_le_client") as mock_init: with mock.patch("certbot._internal.main._get_and_save_cert") as mock_gsc: mock_gsc.return_value = mock.MagicMock() self._call(["certonly", "--manual", "-d", "foo.bar"]) unused_config, auth, unused_installer = mock_init.call_args[0] assert isinstance(auth, manual.Authenticator) with mock.patch('certbot._internal.main.certonly') as mock_certonly: self._call(["auth", "--standalone"]) assert 1 == mock_certonly.call_count @mock.patch('certbot._internal.log.post_arg_parse_setup') def test_rollback(self, _): _, _, _, client = self._call(['rollback']) assert 1 == client.rollback.call_count _, _, _, client = self._call(['rollback', '--checkpoints', '123']) client.rollback.assert_called_once_with( mock.ANY, 123, mock.ANY, mock.ANY) @mock.patch('certbot._internal.cert_manager.update_live_symlinks') def test_update_symlinks(self, mock_cert_manager): self._call_no_clientmock(['update_symlinks']) assert 1 == mock_cert_manager.call_count @mock.patch('certbot._internal.cert_manager.certificates') def test_certificates(self, mock_cert_manager): self._call_no_clientmock(['certificates']) assert 1 == mock_cert_manager.call_count @mock.patch('certbot._internal.cert_manager.delete') def test_delete(self, mock_cert_manager): self._call_no_clientmock(['delete']) assert 1 == mock_cert_manager.call_count @mock.patch('certbot._internal.main.plugins_disco') @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics') @mock.patch('certbot._internal.log.post_arg_parse_setup') def test_plugins(self, _, _det, mock_disco): flags = ['--init', '--prepare', '--authenticators', '--installers'] for args in itertools.chain( *(itertools.combinations(flags, r) for r in range(len(flags)))): self._call(['plugins'] + list(args)) @mock.patch('certbot._internal.main.plugins_disco') @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics') def test_plugins_no_args(self, _det, mock_disco): ifaces: List[interfaces.Plugin] = [] plugins = mock_disco.PluginsRegistry.find_all() stdout = io.StringIO() with test_util.patch_display_util_with_stdout(stdout=stdout): _, stdout, _, _ = self._call(['plugins'], stdout) plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() assert stdout.getvalue().strip() == str(filtered) @mock.patch('certbot._internal.main.plugins_disco') @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics') def test_plugins_no_args_unprivileged(self, _det, mock_disco): ifaces: List[interfaces.Plugin] = [] plugins = mock_disco.PluginsRegistry.find_all() def throw_error(directory, mode, strict): """Raises error.Error.""" _, _, _ = directory, mode, strict raise errors.Error() stdout = io.StringIO() with mock.patch('certbot.util.set_up_core_dir') as mock_set_up_core_dir: with test_util.patch_display_util_with_stdout(stdout=stdout): mock_set_up_core_dir.side_effect = throw_error _, stdout, _, _ = self._call(['plugins'], stdout) plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() assert stdout.getvalue().strip() == str(filtered) @mock.patch('certbot._internal.main.plugins_disco') @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics') def test_plugins_init(self, _det, mock_disco): ifaces: List[interfaces.Plugin] = [] plugins = mock_disco.PluginsRegistry.find_all() stdout = io.StringIO() with test_util.patch_display_util_with_stdout(stdout=stdout): _, stdout, _, _ = self._call(['plugins', '--init'], stdout) plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() assert filtered.init.call_count == 1 assert stdout.getvalue().strip() == str(filtered) @mock.patch('certbot._internal.main.plugins_disco') @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics') def test_plugins_prepare(self, _det, mock_disco): ifaces: List[interfaces.Plugin] = [] plugins = mock_disco.PluginsRegistry.find_all() stdout = io.StringIO() with test_util.patch_display_util_with_stdout(stdout=stdout): _, stdout, _, _ = self._call(['plugins', '--init', '--prepare'], stdout) plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() assert filtered.init.call_count == 1 filtered.prepare.assert_called_once_with() filtered.available.assert_called_once_with() available = filtered.available() assert stdout.getvalue().strip() == str(available) def test_certonly_abspath(self): cert = 'cert' key = 'key' chain = 'chain' fullchain = 'fullchain' with mock.patch('certbot._internal.main.certonly') as mock_certonly: self._call(['certonly', '--cert-path', cert, '--key-path', 'key', '--chain-path', 'chain', '--fullchain-path', 'fullchain']) config, unused_plugins = mock_certonly.call_args[0] assert config.cert_path == os.path.abspath(cert) assert config.key_path == os.path.abspath(key) assert config.chain_path == os.path.abspath(chain) assert config.fullchain_path == os.path.abspath(fullchain) def test_certonly_bad_args(self): try: self._call(['-a', 'bad_auth', 'certonly']) assert False, "Exception should have been raised" except errors.PluginSelectionError as e: assert 'The requested bad_auth plugin does not appear' in str(e) def test_check_config_sanity_domain(self): # FQDN with pytest.raises(errors.ConfigurationError): self._call(['-d', 'a' * 64]) # FQDN 2 with pytest.raises(errors.ConfigurationError): self._call(['-d', (('a' * 50) + '.') * 10]) # Bare IP address (this is actually a different error message now) with pytest.raises(errors.ConfigurationError): self._call(['-d', '204.11.231.35']) # Bare IPv6 address with pytest.raises(errors.ConfigurationError): self._call(['-d', '2001:db8:ac69:3ff:b1cb:c8c6:5a84:a31b']) def test_csr_with_besteffort(self): with pytest.raises(errors.Error): self._call('certonly --csr {0} --allow-subset-of-names'.format(CSR).split()) def test_run_with_csr(self): # This is an error because you can only use --csr with certonly try: self._call(['--csr', CSR]) except errors.Error as e: assert "Please try the certonly" in repr(e) return assert False, "Expected supplying --csr to fail with default verb" def test_csr_with_no_domains(self): with pytest.raises(errors.Error): self._call('certonly --csr {0}'.format( test_util.vector_path('csr-nonames_512.pem')).split()) def test_csr_with_inconsistent_domains(self): with pytest.raises(errors.Error): self._call('certonly -d example.org --csr {0}'.format(CSR).split()) def _certonly_new_request_common(self, mock_client, args=None): with mock.patch('certbot._internal.main._find_lineage_for_domains_and_certname') \ as mock_renewal: mock_renewal.return_value = ("newcert", None) with mock.patch('certbot._internal.main._init_le_client') as mock_init: mock_init.return_value = mock_client if args is None: args = [] args += '-d foo.bar -a standalone certonly'.split() self._call(args) @mock.patch('certbot._internal.main._report_new_cert') def test_certonly_dry_run_new_request_success(self, mock_report): mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = None self._certonly_new_request_common(mock_client, ['--dry-run']) assert mock_client.obtain_and_enroll_certificate.call_count == 1 assert mock_report.call_count == 1 assert mock_report.call_args[0][0].dry_run is True @mock.patch('certbot._internal.main._report_new_cert') @mock.patch('certbot._internal.main.util.atexit_register') @mock.patch('certbot._internal.eff.handle_subscription') @mock.patch('certbot.crypto_util.notAfter') def test_certonly_new_request_success(self, mock_notAfter, mock_subscription, mock_register, mock_report): cert_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/foo.bar')) key_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/baz.qux')) date = '1970-01-01' mock_notAfter().date.return_value = date mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path, fullchain_path=cert_path, key_path=key_path) mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = mock_lineage self._certonly_new_request_common(mock_client) assert mock_client.obtain_and_enroll_certificate.call_count == 1 assert mock_report.call_count == 1 assert cert_path in mock_report.call_args[0][2] assert key_path in mock_report.call_args[0][3] assert 'donate' in mock_register.call_args[0][1] assert mock_subscription.called is True @mock.patch('certbot._internal.eff.handle_subscription') def test_certonly_new_request_failure(self, mock_subscription): mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = False with pytest.raises(errors.Error): self._certonly_new_request_common(mock_client) assert mock_subscription.called is False def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None, args=None, should_renew=True, error_expected=False, quiet_mode=False, expiry_date=datetime.datetime.now(), reuse_key=False, new_key=False): cert_path = test_util.vector_path('cert_512.pem') chain_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/foo.bar/fullchain.pem')) mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path, cert_path=cert_path, fullchain_path=chain_path) mock_lineage.should_autorenew.return_value = due_for_renewal mock_lineage.has_pending_deployment.return_value = False mock_lineage.names.return_value = ['isnot.org'] mock_lineage.private_key_type = 'ecdsa' mock_lineage.elliptic_curve = 'secp256r1' mock_lineage.reuse_key = reuse_key mock_certr = mock.MagicMock() mock_key = mock.MagicMock(pem='pem_key') mock_client = mock.MagicMock() stdout = io.StringIO() mock_client.obtain_certificate.return_value = (mock_certr, 'chain', mock_key, 'csr') def write_msg(message, *args, **kwargs): # pylint: disable=unused-argument """Write message to stdout.""" stdout.write(message) try: with mock.patch('certbot._internal.cert_manager.find_duplicative_certs') as mock_fdc: mock_fdc.return_value = (mock_lineage, None) with mock.patch('certbot._internal.main._init_le_client') as mock_init: mock_init.return_value = mock_client with mock.patch('certbot._internal.display.obj.get_display') as mock_display: if not quiet_mode: mock_display().notification.side_effect = write_msg with mock.patch('certbot._internal.main.renewal.crypto_util') \ as mock_crypto_util: mock_crypto_util.notAfter.return_value = expiry_date with mock.patch('certbot._internal.eff.handle_subscription'): if not args: args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly'] if extra_args: args += extra_args try: ret, stdout, _, _ = self._call(args, stdout) if ret: print("Returned", ret) raise AssertionError(ret) assert not error_expected, "renewal should have errored" except: # pylint: disable=bare-except if not error_expected: raise AssertionError( "Unexpected renewal error:\n" + traceback.format_exc()) if should_renew: if reuse_key and not new_key: # The location of the previous live privkey.pem is passed # to obtain_certificate mock_client.obtain_certificate.assert_called_once_with([mock.ANY], os.path.normpath(os.path.join( self.config.config_dir, "live/sample-renewal/privkey.pem"))) else: mock_client.obtain_certificate.assert_called_once_with([mock.ANY], None) else: assert mock_client.obtain_certificate.call_count == 0 except: self._dump_log() raise finally: if log_out: with open(os.path.join(self.config.logs_dir, "letsencrypt.log")) as lf: assert log_out in lf.read() return mock_lineage, mock_display, stdout @mock.patch('certbot._internal.main._report_new_cert') @mock.patch('certbot._internal.main.util.atexit_register') @mock.patch('certbot.crypto_util.notAfter') def test_certonly_renewal(self, _, mock_register, mock_report): lineage, _, _ = self._test_renewal_common(True, []) assert lineage.save_successor.call_count == 1 lineage.update_all_links_to.assert_called_once_with( lineage.latest_common_version()) assert mock_report.call_count == 1 assert 'fullchain.pem' in mock_report.call_args[0][2] assert 'donate' in mock_register.call_args[0][1] @mock.patch('certbot._internal.main.display_util.notify') @mock.patch('certbot._internal.log.logging.handlers.RotatingFileHandler.doRollover') @mock.patch('certbot.crypto_util.notAfter') def test_certonly_renewal_triggers(self, _, __, mock_notify): # --dry-run should force renewal _, _, _ = self._test_renewal_common(False, ['--dry-run', '--keep'], log_out="simulating renewal") mock_notify.assert_any_call('The dry run was successful.') self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'], log_out="Auto-renewal forced") _, mock_displayer, _ = self._test_renewal_common(False, ['-tvv', '--debug', '--keep'], should_renew=False) assert 'not yet due' in mock_displayer().notification.call_args[0][0] def _dump_log(self): print("Logs:") log_path = os.path.join(self.config.logs_dir, "letsencrypt.log") if os.path.exists(log_path): with open(log_path) as lf: print(lf.read()) def test_renew_verb(self): test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(True, [], args=args, should_renew=True) def test_reuse_key(self): test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf', ec=False) args = ["renew", "--dry-run", "--reuse-key"] self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True) @mock.patch('certbot._internal.storage.RenewableCert.save_successor') def test_reuse_key_no_dry_run(self, unused_save_successor): test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf', ec=False) args = ["renew", "--reuse-key"] self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True) @mock.patch('certbot._internal.storage.RenewableCert.save_successor') def test_new_key(self, unused_save_successor): test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--reuse-key", "--new-key"] self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True, new_key=True) @mock.patch('sys.stdin') def test_noninteractive_renewal_delay(self, stdin): stdin.isatty.return_value = False test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(True, [], args=args, should_renew=True) assert self.mock_sleep.call_count == 1 # in main.py: # sleep_time = random.randint(1, 60*8) sleep_call_arg = self.mock_sleep.call_args[0][0] assert 1 <= sleep_call_arg <= 60*8 @mock.patch('sys.stdin') def test_interactive_no_renewal_delay(self, stdin): stdin.isatty.return_value = True test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(True, [], args=args, should_renew=True) assert self.mock_sleep.call_count == 0 @mock.patch('certbot._internal.renewal.should_renew') def test_renew_skips_recent_certs(self, should_renew): should_renew.return_value = False test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') expiry = datetime.datetime.now() + datetime.timedelta(days=90) _, _, stdout = self._test_renewal_common(False, extra_args=None, should_renew=False, args=['renew'], expiry_date=expiry) assert 'No renewals were attempted.' in stdout.getvalue() assert 'The following certificates are not due for renewal yet:' in stdout.getvalue() @mock.patch('certbot._internal.log.post_arg_parse_setup') def test_quiet_renew(self, _): test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run"] _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) out = stdout.getvalue() assert "renew" in out args = ["renew", "--dry-run", "-q"] _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True, quiet_mode=True) out = stdout.getvalue() assert "" == out def test_renew_hook_validation(self): test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run", "--post-hook=no-such-command"] self._test_renewal_common(True, [], args=args, should_renew=False, error_expected=True) def test_renew_no_hook_validation(self): test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run", "--post-hook=no-such-command", "--disable-hook-validation"] with mock.patch("certbot._internal.hooks.post_hook"): self._test_renewal_common(True, [], args=args, should_renew=True, error_expected=False) def test_renew_verb_empty_config(self): rd = os.path.join(self.config.config_dir, 'renewal') if not os.path.exists(rd): filesystem.makedirs(rd) with open(os.path.join(rd, 'empty.conf'), 'w'): pass # leave the file empty args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(False, [], args=args, should_renew=False, error_expected=True) def test_renew_with_certname(self): test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') self._test_renewal_common(True, [], should_renew=True, args=['renew', '--dry-run', '--cert-name', 'sample-renewal']) def test_renew_with_bad_certname(self): self._test_renewal_common(True, [], should_renew=False, args=['renew', '--dry-run', '--cert-name', 'sample-renewal'], error_expected=True) def _make_dummy_renewal_config(self): renewer_configs_dir = os.path.join(self.config.config_dir, 'renewal') filesystem.makedirs(renewer_configs_dir) with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f: f.write("My contents don't matter") def _test_renew_common(self, renewalparams=None, names=None, assert_oc_called=None, **kwargs): self._make_dummy_renewal_config() with mock.patch('certbot._internal.storage.RenewableCert') as mock_rc: mock_lineage = mock.MagicMock() mock_lineage.fullchain = "somepath/fullchain.pem" if renewalparams is not None: mock_lineage.configuration = {'renewalparams': renewalparams} if names is not None: mock_lineage.names.return_value = names mock_rc.return_value = mock_lineage with mock.patch('certbot._internal.main.renew_cert') as mock_renew_cert: kwargs.setdefault('args', ['renew']) self._test_renewal_common(True, None, should_renew=False, **kwargs) if assert_oc_called is not None: if assert_oc_called: assert mock_renew_cert.called else: assert mock_renew_cert.called is False def test_renew_no_renewalparams(self): self._test_renew_common(assert_oc_called=False, error_expected=True) def test_renew_no_authenticator(self): self._test_renew_common(renewalparams={}, assert_oc_called=False, error_expected=True) def test_renew_with_bad_int(self): renewalparams = {'authenticator': 'webroot', 'rsa_key_size': 'over 9000'} self._test_renew_common(renewalparams=renewalparams, error_expected=True, assert_oc_called=False) def test_renew_with_nonetype_http01(self): renewalparams = {'authenticator': 'webroot', 'http01_port': 'None'} self._test_renew_common(renewalparams=renewalparams, assert_oc_called=True) def test_renew_with_bad_domain(self): renewalparams = {'authenticator': 'webroot'} names = ['uniçodé.com'] self._test_renew_common(renewalparams=renewalparams, error_expected=True, names=names, assert_oc_called=False) @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins') def test_renew_with_configurator(self, mock_sel): mock_sel.return_value = (mock.MagicMock(), mock.MagicMock()) renewalparams = {'authenticator': 'webroot'} self._test_renew_common( renewalparams=renewalparams, assert_oc_called=True, args='renew --configurator apache'.split()) def test_renew_plugin_config_restoration(self): renewalparams = {'authenticator': 'webroot', 'webroot_path': 'None', 'webroot_imaginary_flag': '42'} self._test_renew_common(renewalparams=renewalparams, assert_oc_called=True) def test_renew_with_webroot_map(self): renewalparams = {'authenticator': 'webroot'} self._test_renew_common( renewalparams=renewalparams, assert_oc_called=True, args=['renew', '--webroot-map', json.dumps({'example.com': tempfile.gettempdir()})]) def test_renew_reconstitute_error(self): # pylint: disable=protected-access with mock.patch('certbot._internal.main.renewal.reconstitute') as mock_reconstitute: mock_reconstitute.side_effect = Exception self._test_renew_common(assert_oc_called=False, error_expected=True) def test_renew_obtain_cert_error(self): self._make_dummy_renewal_config() with mock.patch('certbot._internal.storage.RenewableCert') as mock_rc: mock_lineage = mock.MagicMock() mock_lineage.fullchain = "somewhere/fullchain.pem" mock_rc.return_value = mock_lineage mock_lineage.configuration = { 'renewalparams': {'authenticator': 'webroot'}} with mock.patch('certbot._internal.main.renew_cert') as mock_renew_cert: mock_renew_cert.side_effect = Exception self._test_renewal_common(True, None, error_expected=True, args=['renew'], should_renew=False) def test_renew_with_bad_cli_args(self): self._test_renewal_common(True, None, args='renew -d example.com'.split(), should_renew=False, error_expected=True) self._test_renewal_common(True, None, args='renew --csr {0}'.format(CSR).split(), should_renew=False, error_expected=True) def test_no_renewal_with_hooks(self): _, _, stdout = self._test_renewal_common( due_for_renewal=False, extra_args=None, should_renew=False, args=['renew', '--post-hook', '{0} -c "print(\'hello world\');"' .format(sys.executable)]) assert 'No hooks were run.' in stdout.getvalue() @test_util.patch_display_util() @mock.patch('certbot._internal.main._find_lineage_for_domains_and_certname') @mock.patch('certbot._internal.main._init_le_client') @mock.patch('certbot._internal.main._report_new_cert') def test_certonly_reinstall(self, mock_report_new_cert, mock_init, mock_renewal, mock_get_utility): mock_renewal.return_value = ('reinstall', mock.MagicMock()) mock_init.return_value = mock_client = mock.MagicMock() self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly']) assert mock_client.obtain_certificate.called is False assert mock_client.obtain_and_enroll_certificate.called is False assert mock_get_utility().add_message.call_count == 0 mock_report_new_cert.assert_not_called() #self.assertTrue('donate' not in mock_get_utility().add_message.call_args[0][0]) def _test_certonly_csr_common(self, extra_args=None): certr = 'certr' chain = 'chain' mock_client = mock.MagicMock() mock_client.obtain_certificate_from_csr.return_value = (certr, chain) cert_path = os.path.normpath(os.path.join( self.config.config_dir, 'live/example.com/cert_512.pem')) full_path = os.path.normpath(os.path.join( self.config.config_dir, 'live/example.com/fullchain.pem')) mock_client.save_certificate.return_value = cert_path, None, full_path with mock.patch('certbot._internal.main._init_le_client') as mock_init: mock_init.return_value = mock_client chain_path = os.path.normpath(os.path.join( self.config.config_dir, 'live/example.com/chain.pem')) args = ('-a standalone certonly --csr {0} --cert-path {1} ' '--chain-path {2} --fullchain-path {3}').format( CSR, cert_path, chain_path, full_path).split() if extra_args: args += extra_args with mock.patch('certbot._internal.main.crypto_util'): self._call(args) if '--dry-run' in args: assert mock_client.save_certificate.called is False else: mock_client.save_certificate.assert_called_once_with( certr, chain, cert_path, chain_path, full_path) @mock.patch('certbot._internal.main._csr_report_new_cert') @mock.patch('certbot._internal.main.util.atexit_register') @mock.patch('certbot._internal.eff.handle_subscription') def test_certonly_csr(self, mock_subscription, mock_register, mock_csr_report): self._test_certonly_csr_common() assert mock_csr_report.call_count == 1 assert 'cert_512.pem' in mock_csr_report.call_args[0][1] assert mock_csr_report.call_args[0][2] is None assert 'fullchain.pem' in mock_csr_report.call_args[0][3] assert 'donate' in mock_register.call_args[0][1] assert mock_subscription.called is True @mock.patch('certbot._internal.main._csr_report_new_cert') def test_certonly_csr_dry_run(self, mock_csr_report): self._test_certonly_csr_common(['--dry-run']) assert mock_csr_report.call_count == 1 assert mock_csr_report.call_args[0][0].dry_run is True @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.main.client.acme_client') def test_revoke_with_key(self, mock_acme_client, mock_delete_if_appropriate): mock_delete_if_appropriate.return_value = False server = 'foo.bar' self._call_no_clientmock(['--cert-path', SS_CERT_PATH, '--key-path', RSA2048_KEY_PATH, '--server', server, 'revoke']) with open(RSA2048_KEY_PATH, 'rb') as f: assert mock_acme_client.ClientV2.call_count == 1 assert mock_acme_client.ClientNetwork.call_args[0][0] == \ jose.JWK.load(f.read()) with open(SS_CERT_PATH, 'rb') as f: cert = crypto_util.pyopenssl_load_certificate(f.read())[0] mock_revoke = mock_acme_client.ClientV2().revoke mock_revoke.assert_called_once_with( jose.ComparableX509(cert), mock.ANY) def test_revoke_with_key_mismatch(self): server = 'foo.bar' with pytest.raises(errors.Error): self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY, '--server', server, 'revoke']) @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.main._determine_account') def test_revoke_without_key(self, mock_determine_account, mock_delete_if_appropriate): mock_delete_if_appropriate.return_value = False mock_determine_account.return_value = (mock.MagicMock(), None) _, _, _, client = self._call(['--cert-path', CERT, 'revoke']) with open(CERT) as f: cert = crypto_util.pyopenssl_load_certificate(f.read())[0] mock_revoke = client.acme_from_config_key().revoke mock_revoke.assert_called_once_with( jose.ComparableX509(cert), mock.ANY) @mock.patch('certbot._internal.log.post_arg_parse_setup') def test_register(self, _): with mock.patch('certbot._internal.main.client') as mocked_client: acc = mock.MagicMock() acc.id = "imaginary_account" mocked_client.register.return_value = (acc, "worked") self._call_no_clientmock(["register", "--email", "user@example.org"]) # TODO: It would be more correct to explicitly check that # _determine_account() gets called in the above case, # but coverage statistics should also show that it did. with mock.patch('certbot._internal.main.account') as mocked_account: mocked_storage = mock.MagicMock() mocked_account.AccountFileStorage.return_value = mocked_storage mocked_storage.find_all.return_value = ["an account"] x = self._call_no_clientmock(["register", "--email", "user@example.org"]) assert "There is an existing account" in x[0] @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins') @mock.patch('certbot._internal.updater._run_updaters') def test_plugin_selection_error(self, mock_run, mock_choose): mock_choose.side_effect = errors.PluginSelectionError with pytest.raises(errors.PluginSelectionError): main.renew_cert(None, None, None) self.config.dry_run = False updater.run_generic_updaters(self.config, None, None) # Make sure we're returning None, and hence not trying to run the # without installer assert mock_run.called is False @mock.patch('certbot._internal.main.updater.run_renewal_deployer') @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins') @mock.patch('certbot._internal.main._init_le_client') @mock.patch('certbot._internal.main._get_and_save_cert') def test_renew_doesnt_restart_on_dryrun(self, mock_get_cert, mock_init, mock_choose, mock_run_renewal_deployer): """A dry-run renewal shouldn't try to restart the installer""" self.config.dry_run = True installer = mock.MagicMock() mock_choose.return_value = (installer, mock.MagicMock()) main.renew_cert(self.config, None, None) assert mock_init.call_count == 1 assert mock_get_cert.call_count == 1 installer.restart.assert_not_called() mock_run_renewal_deployer.assert_not_called() class UnregisterTest(unittest.TestCase): def setUp(self): self.patchers = { '_determine_account': mock.patch('certbot._internal.main._determine_account'), 'account': mock.patch('certbot._internal.main.account'), 'client': mock.patch('certbot._internal.main.client'), 'get_utility': test_util.patch_display_util()} self.mocks = {k: v.start() for k, v in self.patchers.items()} def tearDown(self): for patch in self.patchers.values(): patch.stop() def test_abort_unregister(self): self.mocks['account'].AccountFileStorage.return_value = mock.Mock() util_mock = self.mocks['get_utility']() util_mock.yesno.return_value = False config = mock.Mock() unused_plugins = mock.Mock() res = main.unregister(config, unused_plugins) assert res == "Deactivation aborted." @mock.patch("certbot._internal.main.display_util.notify") def test_unregister(self, mock_notify): mocked_storage = mock.MagicMock() mocked_storage.find_all.return_value = ["an account"] self.mocks['account'].AccountFileStorage.return_value = mocked_storage self.mocks['_determine_account'].return_value = (mock.MagicMock(), "foo") cb_client = mock.MagicMock() self.mocks['client'].Client.return_value = cb_client config = mock.MagicMock() unused_plugins = mock.MagicMock() res = main.unregister(config, unused_plugins) assert res is None mock_notify.assert_called_once_with("Account deactivated.") def test_unregister_no_account(self): mocked_storage = mock.MagicMock() mocked_storage.find_all.return_value = [] self.mocks['account'].AccountFileStorage.return_value = mocked_storage cb_client = mock.MagicMock() self.mocks['client'].Client.return_value = cb_client config = mock.MagicMock() config.server = "https://acme.example.com/directory" unused_plugins = mock.MagicMock() res = main.unregister(config, unused_plugins) m = "Could not find existing account for server https://acme.example.com/directory." assert res == m assert cb_client.acme.deactivate_registration.called is False class MakeOrVerifyNeededDirs(test_util.ConfigTestCase): """Tests for certbot._internal.main.make_or_verify_needed_dirs.""" @mock.patch("certbot._internal.main.util") def test_it(self, mock_util): main.make_or_verify_needed_dirs(self.config) for core_dir in (self.config.config_dir, self.config.work_dir,): mock_util.set_up_core_dir.assert_any_call( core_dir, constants.CONFIG_DIRS_MODE, self.config.strict_permissions ) hook_dirs = (self.config.renewal_pre_hooks_dir, self.config.renewal_deploy_hooks_dir, self.config.renewal_post_hooks_dir,) for hook_dir in hook_dirs: # default mode of 755 is used mock_util.make_or_verify_dir.assert_any_call( hook_dir, strict=self.config.strict_permissions) class EnhanceTest(test_util.ConfigTestCase): """Tests for certbot._internal.main.enhance.""" def setUp(self): super().setUp() self.get_utility_patch = test_util.patch_display_util() self.mock_get_utility = self.get_utility_patch.start() self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement) def tearDown(self): self.get_utility_patch.stop() def _call(self, args): plugins = disco.PluginsRegistry.find_all() config = cli.prepare_and_parse_args(plugins, args) with mock.patch('certbot._internal.cert_manager.get_certnames') as mock_certs: mock_certs.return_value = ['example.com'] with mock.patch('certbot._internal.cert_manager.domains_for_certname') as mock_dom: mock_dom.return_value = ['example.com'] with mock.patch('certbot._internal.main._init_le_client') as mock_init: mock_client = mock.MagicMock() mock_client.config = config mock_init.return_value = mock_client main.enhance(config, plugins) return mock_client # returns the client @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.main.display_ops.choose_values') @mock.patch('certbot._internal.main._find_domains_or_certname') def test_selection_question(self, mock_find, mock_choose, mock_lineage, _rec): mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent") mock_choose.return_value = ['example.com'] mock_find.return_value = (None, None) with mock.patch('certbot._internal.main.plug_sel.pick_installer') as mock_pick: self._call(['enhance', '--redirect']) assert mock_pick.called # Check that the message includes "enhancements" assert "enhancements" in mock_pick.call_args[0][3] @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.main.display_ops.choose_values') @mock.patch('certbot._internal.main._find_domains_or_certname') def test_selection_auth_warning(self, mock_find, mock_choose, mock_lineage, _rec): mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent") mock_choose.return_value = ["example.com"] mock_find.return_value = (None, None) with mock.patch('certbot._internal.main.plug_sel.pick_installer'): with mock.patch('certbot._internal.main.plug_sel.logger.warning') as mock_log: mock_client = self._call(['enhance', '-a', 'webroot', '--redirect']) assert mock_log.called assert "make sense" in mock_log.call_args[0][0] assert mock_client.enhance_config.called @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.main.display_ops.choose_values') @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') def test_enhance_config_call(self, _rec, mock_choose, mock_lineage): mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent") mock_choose.return_value = ["example.com"] with mock.patch('certbot._internal.main.plug_sel.pick_installer'): mock_client = self._call(['enhance', '--redirect', '--hsts']) req_enh = ["redirect", "hsts"] not_req_enh = ["uir"] assert mock_client.enhance_config.called assert all(getattr(mock_client.config, e) for e in req_enh) assert not any(getattr(mock_client.config, e) for e in not_req_enh) assert "example.com" in mock_client.enhance_config.call_args[0][0] @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.main.display_ops.choose_values') @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') def test_enhance_noninteractive(self, _rec, mock_choose, mock_lineage): mock_lineage.return_value = mock.MagicMock( chain_path="/tmp/nonexistent") mock_choose.return_value = ["example.com"] with mock.patch('certbot._internal.main.plug_sel.pick_installer'): mock_client = self._call(['enhance', '--redirect', '--hsts', '--non-interactive']) assert mock_client.enhance_config.called assert mock_choose.called is False @mock.patch('certbot._internal.main.display_ops.choose_values') @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') def test_user_abort_domains(self, _rec, mock_choose): mock_choose.return_value = [] with mock.patch('certbot._internal.main.plug_sel.pick_installer'): with pytest.raises(errors.Error): self._call(['enhance', '--redirect', '--hsts']) def test_no_enhancements_defined(self): with pytest.raises(errors.MisconfigurationError): self._call(['enhance', '-a', 'null']) @mock.patch('certbot._internal.main.plug_sel.choose_configurator_plugins') @mock.patch('certbot._internal.main.display_ops.choose_values') @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') def test_plugin_selection_error(self, _rec, mock_choose, mock_pick): mock_choose.return_value = ["example.com"] mock_pick.return_value = (None, None) mock_pick.side_effect = errors.PluginSelectionError() mock_client = self._call(['enhance', '--hsts']) assert mock_client.enhance_config.called is False @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.main.display_ops.choose_values') @mock.patch('certbot._internal.main.plug_sel.pick_installer') @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @test_util.patch_display_util() def test_enhancement_enable(self, _, _rec, mock_inst, mock_choose, mock_lineage): mock_inst.return_value = self.mockinstaller mock_choose.return_value = ["example.com", "another.tld"] mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent") self._call(['enhance', '--auto-hsts']) assert self.mockinstaller.enable_autohsts.called assert self.mockinstaller.enable_autohsts.call_args[0][1] == \ ["example.com", "another.tld"] @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.main.display_ops.choose_values') @mock.patch('certbot._internal.main.plug_sel.pick_installer') @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @test_util.patch_display_util() def test_enhancement_enable_not_supported(self, _, _rec, mock_inst, mock_choose, mock_lineage): mock_inst.return_value = null.Installer(self.config, "null") mock_choose.return_value = ["example.com", "another.tld"] mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent") with pytest.raises(errors.NotSupportedError): self._call(['enhance', '--auto-hsts']) def test_enhancement_enable_conflict(self): with pytest.raises(errors.Error): self._call(['enhance', '--auto-hsts', '--hsts']) class InstallTest(test_util.ConfigTestCase): """Tests for certbot._internal.main.install.""" def setUp(self): super().setUp() self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement) @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @mock.patch('certbot._internal.main.plug_sel.pick_installer') def test_install_enhancement_not_supported(self, mock_inst, _rec): mock_inst.return_value = null.Installer(self.config, "null") plugins = disco.PluginsRegistry.find_all() self.config.auto_hsts = True self.config.certname = "nonexistent" with pytest.raises(errors.NotSupportedError): main.install(self.config, plugins) @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins') @mock.patch('certbot._internal.main.plug_sel.pick_installer') def test_install_enhancement_no_certname(self, mock_inst, _rec): mock_inst.return_value = self.mockinstaller plugins = disco.PluginsRegistry.find_all() self.config.auto_hsts = True self.config.certname = None self.config.key_path = "/tmp/nonexistent" self.config.cert_path = "/tmp/nonexistent" with pytest.raises(errors.ConfigurationError): main.install(self.config, plugins) class ReportNewCertTest(unittest.TestCase): """Tests for certbot._internal.main._report_new_cert and certbot._internal.main._csr_report_new_cert. """ def setUp(self): self.notify_patch = mock.patch('certbot._internal.main.display_util.notify') self.mock_notify = self.notify_patch.start() self.notafter_patch = mock.patch('certbot._internal.main.crypto_util.notAfter') self.mock_notafter = self.notafter_patch.start() self.mock_notafter.return_value = datetime.datetime(1970, 1, 1, 0, 0) def tearDown(self): self.notify_patch.stop() self.notafter_patch.stop() @classmethod def _call(cls, *args, **kwargs): from certbot._internal.main import _report_new_cert return _report_new_cert(*args, **kwargs) @classmethod def _call_csr(cls, *args, **kwargs): from certbot._internal.main import _csr_report_new_cert return _csr_report_new_cert(*args, **kwargs) def test_report_dry_run(self): self._call(mock.Mock(dry_run=True), None, None, None) self.mock_notify.assert_called_with("The dry run was successful.") def test_csr_report_dry_run(self): self._call_csr(mock.Mock(dry_run=True), None, None, None) self.mock_notify.assert_called_with("The dry run was successful.") def test_report_no_paths(self): with pytest.raises(AssertionError): self._call(mock.Mock(dry_run=False), None, None, None) with pytest.raises(AssertionError): self._call_csr(mock.Mock(dry_run=False), None, None, None) def test_report(self): self._call(mock.Mock(dry_run=False), '/path/to/cert.pem', '/path/to/fullchain.pem', '/path/to/privkey.pem') self.mock_notify.assert_called_with( '\nSuccessfully received certificate.\n' 'Certificate is saved at: /path/to/fullchain.pem\n' 'Key is saved at: /path/to/privkey.pem\n' 'This certificate expires on 1970-01-01.\n' 'These files will be updated when the certificate renews.\n' 'Certbot has set up a scheduled task to automatically renew this ' 'certificate in the background.' ) def test_report_no_key(self): self._call(mock.Mock(dry_run=False), '/path/to/cert.pem', '/path/to/fullchain.pem', None) self.mock_notify.assert_called_with( '\nSuccessfully received certificate.\n' 'Certificate is saved at: /path/to/fullchain.pem\n' 'This certificate expires on 1970-01-01.\n' 'These files will be updated when the certificate renews.\n' 'Certbot has set up a scheduled task to automatically renew this ' 'certificate in the background.' ) def test_report_no_preconfigured_renewal(self): self._call(mock.Mock(dry_run=False, preconfigured_renewal=False), '/path/to/cert.pem', '/path/to/fullchain.pem', '/path/to/privkey.pem') self.mock_notify.assert_called_with( '\nSuccessfully received certificate.\n' 'Certificate is saved at: /path/to/fullchain.pem\n' 'Key is saved at: /path/to/privkey.pem\n' 'This certificate expires on 1970-01-01.\n' 'These files will be updated when the certificate renews.' ) def test_csr_report(self): self._call_csr(mock.Mock(dry_run=False), '/path/to/cert.pem', '/path/to/chain.pem', '/path/to/fullchain.pem') self.mock_notify.assert_called_with( '\nSuccessfully received certificate.\n' 'Certificate is saved at: /path/to/cert.pem\n' 'Intermediate CA chain is saved at: /path/to/chain.pem\n' 'Full certificate chain is saved at: /path/to/fullchain.pem\n' 'This certificate expires on 1970-01-01.' ) def test_manual_no_hooks_report(self): """Shouldn't get a message about autorenewal if no --manual-auth-hook""" self._call(mock.Mock(dry_run=False, authenticator='manual', manual_auth_hook=None), '/path/to/cert.pem', '/path/to/fullchain.pem', '/path/to/privkey.pem') self.mock_notify.assert_called_with( '\nSuccessfully received certificate.\n' 'Certificate is saved at: /path/to/fullchain.pem\n' 'Key is saved at: /path/to/privkey.pem\n' 'This certificate expires on 1970-01-01.\n' 'These files will be updated when the certificate renews.' ) class ReportNextStepsTest(unittest.TestCase): """Tests for certbot._internal.main._report_next_steps""" def setUp(self): self.config = mock.MagicMock( cert_name="example.com", preconfigured_renewal=True, csr=None, authenticator="nginx", manual_auth_hook=None) notify_patch = mock.patch('certbot._internal.main.display_util.notify') self.mock_notify = notify_patch.start() self.addCleanup(notify_patch.stop) self.old_stdout = sys.stdout sys.stdout = io.StringIO() def tearDown(self): sys.stdout = self.old_stdout @classmethod def _call(cls, *args, **kwargs): from certbot._internal.main import _report_next_steps _report_next_steps(*args, **kwargs) def _output(self) -> str: assert self.mock_notify.call_count == 2 assert self.mock_notify.call_args_list[0][0][0] == 'NEXT STEPS:' return self.mock_notify.call_args_list[1][0][0] def test_report(self): """No steps for a normal renewal""" self.config.authenticator = "manual" self.config.manual_auth_hook = "/bin/true" self._call(self.config, None, None) self.mock_notify.assert_not_called() def test_csr_report(self): """--csr requires manual renewal""" self.config.csr = "foo.csr" self._call(self.config, None, None) assert "--csr will not be renewed" in self._output() def test_manual_no_hook_renewal(self): """--manual without a hook requires manual renewal""" self.config.authenticator = "manual" self._call(self.config, None, None) assert "--manual certificates requires" in self._output() def test_no_preconfigured_renewal(self): """No --preconfigured-renewal needs manual cron setup""" self.config.preconfigured_renewal = False self._call(self.config, None, None) assert "https://certbot.org/renewal-setup" in self._output() class UpdateAccountTest(test_util.ConfigTestCase): """Tests for certbot._internal.main.update_account""" def setUp(self): patches = { 'account': mock.patch('certbot._internal.main.account'), 'atexit': mock.patch('certbot.util.atexit'), 'client': mock.patch('certbot._internal.main.client'), 'determine_account': mock.patch('certbot._internal.main._determine_account'), 'notify': mock.patch('certbot._internal.main.display_util.notify'), 'prepare_sub': mock.patch('certbot._internal.eff.prepare_subscription'), 'util': test_util.patch_display_util() } self.mocks = { k: patches[k].start() for k in patches } for patch in patches.values(): self.addCleanup(patch.stop) return super().setUp() def _call(self, args): with mock.patch('certbot._internal.main.sys.stdout'), \ mock.patch('certbot._internal.main.sys.stderr'): args = ['--config-dir', self.config.config_dir, '--work-dir', self.config.work_dir, '--logs-dir', self.config.logs_dir, '--text'] + args return main.main(args[:]) # NOTE: parser can alter its args! def _prepare_mock_account(self): mock_storage = mock.MagicMock() mock_account = mock.MagicMock() mock_regr = mock.MagicMock() mock_storage.find_all.return_value = [mock_account] self.mocks['account'].AccountFileStorage.return_value = mock_storage mock_account.regr.body = mock_regr.body self.mocks['determine_account'].return_value = (mock_account, mock.MagicMock()) return (mock_account, mock_storage, mock_regr) def _test_update_no_contact(self, args): """Utility to assert that email removal is handled correctly""" (_, mock_storage, mock_regr) = self._prepare_mock_account() result = self._call(args) # When update succeeds, the return value of update_account() is None assert result is None # We submitted a registration to the server assert self.mocks['client'].Client().acme.update_registration.call_count == 1 mock_regr.body.update.assert_called_with(contact=()) # We got an update from the server and persisted it assert mock_storage.update_regr.call_count == 1 # We should have notified the user self.mocks['notify'].assert_called_with( 'Any contact information associated with this account has been removed.' ) # We should not have called subscription because there's no email self.mocks['prepare_sub'].assert_not_called() def test_no_existing_accounts(self): """Test that no existing account is handled correctly""" mock_storage = mock.MagicMock() mock_storage.find_all.return_value = [] self.mocks['account'].AccountFileStorage.return_value = mock_storage assert self._call(['update_account', '--email', 'user@example.org']) == \ 'Could not find an existing account for server' \ ' https://acme-v02.api.letsencrypt.org/directory.' def test_update_account_remove_email(self): """Test that --register-unsafely-without-email is handled as no email""" self._test_update_no_contact(['update_account', '--register-unsafely-without-email']) def test_update_account_empty_email(self): """Test that providing an empty email is handled as no email""" self._test_update_no_contact(['update_account', '-m', '']) @mock.patch('certbot._internal.main.display_ops.get_email') def test_update_account_with_email(self, mock_email): """Test that updating with a singular email is handled correctly""" mock_email.return_value = 'user@example.com' (_, mock_storage, _) = self._prepare_mock_account() mock_client = mock.MagicMock() self.mocks['client'].Client.return_value = mock_client result = self._call(['update_account']) # None if registration succeeds assert result is None # We should have updated the server assert mock_client.acme.update_registration.call_count == 1 # We should have updated the account on disk assert mock_storage.update_regr.call_count == 1 # Subscription should have been prompted assert self.mocks['prepare_sub'].call_count == 1 # Should have printed the email self.mocks['notify'].assert_called_with( 'Your e-mail address was updated to user@example.com.') def test_update_account_with_multiple_emails(self): """Test that multiple email addresses are handled correctly""" (_, mock_storage, mock_regr) = self._prepare_mock_account() assert self._call(['update_account', '-m', 'user@example.com,user@example.org']) is None mock_regr.body.update.assert_called_with( contact=['mailto:user@example.com', 'mailto:user@example.org'] ) assert mock_storage.update_regr.call_count == 1 self.mocks['notify'].assert_called_with( 'Your e-mail address was updated to user@example.com,user@example.org.') class ShowAccountTest(test_util.ConfigTestCase): """Tests for certbot._internal.main.show_account""" def setUp(self): patches = { 'account': mock.patch('certbot._internal.main.account'), 'atexit': mock.patch('certbot.util.atexit'), 'client': mock.patch('certbot._internal.main.client'), 'determine_account': mock.patch('certbot._internal.main._determine_account'), 'notify': mock.patch('certbot._internal.main.display_util.notify'), 'util': test_util.patch_display_util() } self.mocks = { k: patches[k].start() for k in patches } for patch in patches.values(): self.addCleanup(patch.stop) return super().setUp() def _call(self, args): with mock.patch('certbot._internal.main.sys.stdout'), \ mock.patch('certbot._internal.main.sys.stderr'): args = ['--config-dir', self.config.config_dir, '--work-dir', self.config.work_dir, '--logs-dir', self.config.logs_dir, '--text'] + args return main.main(args[:]) # NOTE: parser can alter its args! def _prepare_mock_account(self): mock_storage = mock.MagicMock() mock_account = mock.MagicMock() mock_regr = mock.MagicMock() mock_storage.find_all.return_value = [mock_account] self.mocks['account'].AccountFileStorage.return_value = mock_storage mock_account.regr.body = mock_regr.body mock_account.key.thumbprint.return_value = b'foobarbaz' self.mocks['determine_account'].return_value = (mock_account, mock.MagicMock()) def _test_show_account(self, contact): self._prepare_mock_account() mock_client = mock.MagicMock() mock_regr = mock.MagicMock() mock_regr.body.contact = contact mock_regr.uri = 'https://www.letsencrypt-demo.org/acme/reg/1' mock_client.acme.query_registration.return_value = mock_regr self.mocks['client'].Client.return_value = mock_client args = ['show_account'] self._call(args) assert mock_client.acme.query_registration.call_count == 1 def test_no_existing_accounts(self): """Test that no existing account is handled correctly""" mock_storage = mock.MagicMock() mock_storage.find_all.return_value = [] self.mocks['account'].AccountFileStorage.return_value = mock_storage assert self._call(['show_account']) == \ 'Could not find an existing account for server' \ ' https://acme-v02.api.letsencrypt.org/directory.' def test_no_existing_client(self): """Test that issues with the ACME client are handled correctly""" self._prepare_mock_account() mock_client = mock.MagicMock() mock_client.acme = None self.mocks['client'].Client.return_value = mock_client try: self._call(['show_account']) except errors.Error as e: assert 'ACME client is not set.' == str(e) def test_no_contacts(self): self._test_show_account(()) assert self.mocks['notify'].call_count == 1 self.mocks['notify'].assert_has_calls([ mock.call('Account details for server https://acme-v02.api.letsencr' 'ypt.org/directory:\n Account URL: https://www.letsencry' 'pt-demo.org/acme/reg/1\n Account Thumbprint: Zm9vYmFyYmF6\n' ' Email contact: none')]) def test_single_email(self): contact = ('mailto:foo@example.com',) self._test_show_account(contact) assert self.mocks['notify'].call_count == 1 self.mocks['notify'].assert_has_calls([ mock.call('Account details for server https://acme-v02.api.letsencr' 'ypt.org/directory:\n Account URL: https://www.letsencry' 'pt-demo.org/acme/reg/1\n Account Thumbprint: Zm9vYmFyYmF6' '\n Email contact: foo@example.com')]) def test_double_email(self): contact = ('mailto:foo@example.com', 'mailto:bar@example.com') self._test_show_account(contact) assert self.mocks['notify'].call_count == 1 self.mocks['notify'].assert_has_calls([ mock.call('Account details for server https://acme-v02.api.letsencr' 'ypt.org/directory:\n Account URL: https://www.letsencry' 'pt-demo.org/acme/reg/1\n Account Thumbprint: Zm9vYmFyYmF6\n' ' Email contacts: foo@example.com, bar@example.com')]) class TestLockOrder: """Tests that Certbot's directory locks were acquired in the right order.""" EXPECTED_ERROR_TYPE = errors.Error EXPECTED_ERROR_STR = 'Expected TestLockOrder error' # This regex is needed because certbot renew captures raised errors and # raises its own. EXPECTED_ERROR_STR_REGEX = f'{EXPECTED_ERROR_STR}|1 renew failure' @pytest.fixture def mock_lock_dir(self): with mock.patch('certbot._internal.lock.lock_dir') as mock_lock_dir: yield mock_lock_dir @contextlib.contextmanager def mock_plugin_prepare(self, authenticator_dir, installer_dir, mock_lock_dir, subcommand): """Patches plugin prepare to call mock_lock_dir and raise the expected error.""" def authenticator_lock(unused_self): mock_lock_dir(authenticator_dir) raise self.EXPECTED_ERROR_TYPE(self.EXPECTED_ERROR_STR) def installer_lock(unused_self): mock_lock_dir(installer_dir) # Unless an installer isn't needed (e.g. certbot install), we # expect the authenticator to raise the expected error because it # is prepared last. See # https://github.com/certbot/certbot/blob/7a6752a68ed77e73c2b29ab20d3ca8927f4fa7b0/certbot/certbot/_internal/plugins/selection.py#L246-L249 if subcommand == 'install': raise self.EXPECTED_ERROR_TYPE(self.EXPECTED_ERROR_STR) with mock.patch.object(standalone.Authenticator, 'prepare', authenticator_lock): with mock.patch.object(null.Installer, 'prepare', installer_lock): yield @pytest.fixture(params='certonly install renew run'.split()) def args_and_lock_order(self, mock_lock_dir, request, tmp_path): """Sets up Certbot with args and mocks to error after acquiring the last lock. This fixture yields the CLI arguments that should be given to Certbot and the expected order of directories to be locked. An error is raised after acquiring the last lock just as a means of stopping Certbot's execution. """ # select directories authenticator_dir = str(tmp_path / 'authenticator') config_dir = str(tmp_path / 'config') installer_dir = str(tmp_path / 'installer') logs_dir = str(tmp_path / 'logs') work_dir = str(tmp_path / 'work') # prepare args and lineage subcommand = request.param args = [subcommand, '-a', 'standalone', '-i', 'null', '--no-random-sleep-on-renew', '--config-dir', config_dir, '--logs-dir', logs_dir, '--work-dir', work_dir] test_util.make_lineage(config_dir, 'sample-renewal.conf') with self.mock_plugin_prepare(authenticator_dir, installer_dir, mock_lock_dir, subcommand): lock_order = [logs_dir, config_dir, work_dir, installer_dir] if subcommand == 'install': yield args, lock_order else: # We expect the installer to be prepared even for certonly # because an installer was requested on the command line. yield args, lock_order + [authenticator_dir] def test_lock_order(self, args_and_lock_order, mock_lock_dir): args, lock_order = args_and_lock_order with pytest.raises(self.EXPECTED_ERROR_TYPE, match=self.EXPECTED_ERROR_STR_REGEX): main.main(args) assert mock_lock_dir.call_count == len(lock_order) for call, locked_dir in zip(mock_lock_dir.call_args_list, lock_order): assert call[0][0] == locked_dir if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/ocsp_test.py0000664000175100017510000004217714561227515021337 0ustar00ericaerica"""Tests for ocsp.py""" # pylint: disable=protected-access import contextlib from datetime import datetime from datetime import timedelta import sys import unittest from unittest import mock from cryptography import x509 from cryptography.exceptions import InvalidSignature from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.x509 import ocsp as ocsp_lib import pytest import pytz from certbot import errors from certbot.tests import util as test_util out = """Missing = in header key=value ocsp: Use -help for summary. """ class OCSPTestOpenSSL(unittest.TestCase): """ OCSP revocation tests using OpenSSL binary. """ def setUp(self): from certbot import ocsp with mock.patch('certbot.ocsp.subprocess.run') as mock_run: with mock.patch('certbot.util.exe_exists') as mock_exists: mock_run.stderr = out mock_exists.return_value = True self.checker = ocsp.RevocationChecker(enforce_openssl_binary_usage=True) @mock.patch('certbot.ocsp.logger.info') @mock.patch('certbot.ocsp.subprocess.run') @mock.patch('certbot.util.exe_exists') def test_init(self, mock_exists, mock_run, mock_log): mock_run.return_value.stderr = out mock_exists.return_value = True from certbot import ocsp checker = ocsp.RevocationChecker(enforce_openssl_binary_usage=True) assert mock_run.call_count == 1 assert checker.host_args("x") == ["Host=x"] mock_run.return_value.stderr = out.partition("\n")[2] checker = ocsp.RevocationChecker(enforce_openssl_binary_usage=True) assert checker.host_args("x") == ["Host", "x"] assert checker.broken is False mock_exists.return_value = False mock_run.call_count = 0 checker = ocsp.RevocationChecker(enforce_openssl_binary_usage=True) assert mock_run.call_count == 0 assert mock_log.call_count == 1 assert checker.broken is True @mock.patch('certbot.ocsp._determine_ocsp_server') @mock.patch('certbot.ocsp.crypto_util.notAfter') @mock.patch('certbot.util.run_script') def test_ocsp_revoked(self, mock_run, mock_na, mock_determine): now = datetime.now(pytz.UTC) cert_obj = mock.MagicMock() cert_obj.cert_path = "x" cert_obj.chain_path = "y" mock_na.return_value = now + timedelta(hours=2) self.checker.broken = True mock_determine.return_value = ("", "") assert self.checker.ocsp_revoked(cert_obj) is False self.checker.broken = False mock_run.return_value = tuple(openssl_happy[1:]) assert self.checker.ocsp_revoked(cert_obj) is False assert mock_run.call_count == 0 mock_determine.return_value = ("http://x.co", "x.co") assert self.checker.ocsp_revoked(cert_obj) is False mock_run.side_effect = errors.SubprocessError("Unable to load certificate launcher") assert self.checker.ocsp_revoked(cert_obj) is False assert mock_run.call_count == 2 # cert expired mock_na.return_value = now mock_determine.return_value = ("", "") count_before = mock_determine.call_count assert self.checker.ocsp_revoked(cert_obj) is False assert mock_determine.call_count == count_before def test_determine_ocsp_server(self): cert_path = test_util.vector_path('ocsp_certificate.pem') from certbot import ocsp result = ocsp._determine_ocsp_server(cert_path) assert ('http://ocsp.test4.buypass.com', 'ocsp.test4.buypass.com') == result @mock.patch('certbot.ocsp.logger') @mock.patch('certbot.util.run_script') def test_translate_ocsp(self, mock_run, mock_log): # pylint: disable=protected-access mock_run.return_value = openssl_confused from certbot import ocsp assert ocsp._translate_ocsp_query(*openssl_happy) is False assert ocsp._translate_ocsp_query(*openssl_confused) is False assert mock_log.debug.call_count == 1 assert mock_log.warning.call_count == 0 mock_log.debug.call_count = 0 assert ocsp._translate_ocsp_query(*openssl_unknown) is False assert mock_log.debug.call_count == 1 assert mock_log.warning.call_count == 0 assert ocsp._translate_ocsp_query(*openssl_expired_ocsp) is False assert mock_log.debug.call_count == 2 assert ocsp._translate_ocsp_query(*openssl_broken) is False assert mock_log.warning.call_count == 1 mock_log.info.call_count = 0 assert ocsp._translate_ocsp_query(*openssl_revoked) is True assert mock_log.info.call_count == 0 assert ocsp._translate_ocsp_query(*openssl_expired_ocsp_revoked) is True assert mock_log.info.call_count == 1 class OSCPTestCryptography(unittest.TestCase): """ OCSP revokation tests using Cryptography >= 2.4.0 """ def setUp(self): from certbot import ocsp self.checker = ocsp.RevocationChecker() self.cert_path = test_util.vector_path('ocsp_certificate.pem') self.chain_path = test_util.vector_path('ocsp_issuer_certificate.pem') self.cert_obj = mock.MagicMock() self.cert_obj.cert_path = self.cert_path self.cert_obj.chain_path = self.chain_path now = datetime.now(pytz.UTC) self.mock_notAfter = mock.patch('certbot.ocsp.crypto_util.notAfter', return_value=now + timedelta(hours=2)) self.mock_notAfter.start() # Ensure the mock.patch is stopped even if test raises an exception self.addCleanup(self.mock_notAfter.stop) @mock.patch('certbot.ocsp._determine_ocsp_server') @mock.patch('certbot.ocsp._check_ocsp_cryptography') def test_ensure_cryptography_toggled(self, mock_check, mock_determine): mock_determine.return_value = ('http://example.com', 'example.com') self.checker.ocsp_revoked(self.cert_obj) mock_check.assert_called_once_with(self.cert_path, self.chain_path, 'http://example.com', 10) def test_revoke(self): with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL): revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked def test_responder_is_issuer(self): issuer = x509.load_pem_x509_certificate( test_util.load_vector('ocsp_issuer_certificate.pem'), default_backend()) with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks: # OCSP response with ResponseID as Name mocks['mock_response'].return_value.responder_name = issuer.subject mocks['mock_response'].return_value.responder_key_hash = None self.checker.ocsp_revoked(self.cert_obj) # OCSP response with ResponseID as KeyHash key_hash = x509.SubjectKeyIdentifier.from_public_key(issuer.public_key()).digest mocks['mock_response'].return_value.responder_name = None mocks['mock_response'].return_value.responder_key_hash = key_hash self.checker.ocsp_revoked(self.cert_obj) # Here responder and issuer are the same. So only the signature of the OCSP # response is checked (using the issuer/responder public key). assert mocks['mock_check'].call_count == 2 assert mocks['mock_check'].call_args_list[0][0][0].public_numbers() == \ issuer.public_key().public_numbers() assert mocks['mock_check'].call_args_list[1][0][0].public_numbers() == \ issuer.public_key().public_numbers() def test_responder_is_authorized_delegate(self): issuer = x509.load_pem_x509_certificate( test_util.load_vector('ocsp_issuer_certificate.pem'), default_backend()) responder = x509.load_pem_x509_certificate( test_util.load_vector('ocsp_responder_certificate.pem'), default_backend()) with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks: # OCSP response with ResponseID as Name mocks['mock_response'].return_value.responder_name = responder.subject mocks['mock_response'].return_value.responder_key_hash = None self.checker.ocsp_revoked(self.cert_obj) # OCSP response with ResponseID as KeyHash key_hash = x509.SubjectKeyIdentifier.from_public_key(responder.public_key()).digest mocks['mock_response'].return_value.responder_name = None mocks['mock_response'].return_value.responder_key_hash = key_hash self.checker.ocsp_revoked(self.cert_obj) # Here responder and issuer are not the same. Two signatures will be checked then, # first to verify the responder cert (using the issuer public key), second to # to verify the OCSP response itself (using the responder public key). assert mocks['mock_check'].call_count == 4 assert mocks['mock_check'].call_args_list[0][0][0].public_numbers() == \ issuer.public_key().public_numbers() assert mocks['mock_check'].call_args_list[1][0][0].public_numbers() == \ responder.public_key().public_numbers() assert mocks['mock_check'].call_args_list[2][0][0].public_numbers() == \ issuer.public_key().public_numbers() assert mocks['mock_check'].call_args_list[3][0][0].public_numbers() == \ responder.public_key().public_numbers() def test_revoke_resiliency(self): # Server return an invalid HTTP response with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL, http_status_code=400): revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False # OCSP response in invalid with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.UNAUTHORIZED): revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False # OCSP response is valid, but certificate status is unknown with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL): revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False # The OCSP response says that the certificate is revoked, but certificate # does not contain the OCSP extension. with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL): with mock.patch('cryptography.x509.Extensions.get_extension_for_class', side_effect=x509.ExtensionNotFound( 'Not found', x509.AuthorityInformationAccessOID.OCSP)): revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False # OCSP response uses an unsupported signature. with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL, check_signature_side_effect=UnsupportedAlgorithm('foo')): revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False # OSCP signature response is invalid. with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL, check_signature_side_effect=InvalidSignature('foo')): revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False # Assertion error on OCSP response validity with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL, check_signature_side_effect=AssertionError('foo')): revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False # No responder cert in OCSP response with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks: mocks['mock_response'].return_value.certificates = [] revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False # Responder cert is not signed by certificate issuer with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks: cert = mocks['mock_response'].return_value.certificates[0] mocks['mock_response'].return_value.certificates[0] = mock.Mock( issuer='fake', subject=cert.subject) revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL): # This mock is necessary to avoid the first call contained in _determine_ocsp_server # of the method cryptography.x509.Extensions.get_extension_for_class. with mock.patch('certbot.ocsp._determine_ocsp_server') as mock_server: mock_server.return_value = ('https://example.com', 'example.com') with mock.patch('cryptography.x509.Extensions.get_extension_for_class', side_effect=x509.ExtensionNotFound( 'Not found', x509.AuthorityInformationAccessOID.OCSP)): revoked = self.checker.ocsp_revoked(self.cert_obj) assert revoked is False @contextlib.contextmanager def _ocsp_mock(certificate_status, response_status, http_status_code=200, check_signature_side_effect=None): with mock.patch('certbot.ocsp.ocsp.load_der_ocsp_response') as mock_response: mock_response.return_value = _construct_mock_ocsp_response( certificate_status, response_status) with mock.patch('certbot.ocsp.requests.post') as mock_post: mock_post.return_value = mock.Mock(status_code=http_status_code) with mock.patch('certbot.ocsp.crypto_util.verify_signed_payload') \ as mock_check: if check_signature_side_effect: mock_check.side_effect = check_signature_side_effect yield { 'mock_response': mock_response, 'mock_post': mock_post, 'mock_check': mock_check, } def _construct_mock_ocsp_response(certificate_status, response_status): cert = x509.load_pem_x509_certificate( test_util.load_vector('ocsp_certificate.pem'), default_backend()) issuer = x509.load_pem_x509_certificate( test_util.load_vector('ocsp_issuer_certificate.pem'), default_backend()) responder = x509.load_pem_x509_certificate( test_util.load_vector('ocsp_responder_certificate.pem'), default_backend()) builder = ocsp_lib.OCSPRequestBuilder() builder = builder.add_certificate(cert, issuer, hashes.SHA1()) request = builder.build() return mock.Mock( response_status=response_status, certificate_status=certificate_status, serial_number=request.serial_number, issuer_key_hash=request.issuer_key_hash, issuer_name_hash=request.issuer_name_hash, responder_name=responder.subject, certificates=[responder], hash_algorithm=hashes.SHA1(), next_update=datetime.now(pytz.UTC).replace(tzinfo=None) + timedelta(days=1), this_update=datetime.now(pytz.UTC).replace(tzinfo=None) - timedelta(days=1), signature_algorithm_oid=x509.oid.SignatureAlgorithmOID.RSA_WITH_SHA1, ) # pylint: disable=line-too-long openssl_confused = ("", """ /etc/letsencrypt/live/example.org/cert.pem: good This Update: Dec 17 00:00:00 2016 GMT Next Update: Dec 24 00:00:00 2016 GMT """, """ Response Verify Failure 139903674214048:error:27069065:OCSP routines:OCSP_basic_verify:certificate verify error:ocsp_vfy.c:138:Verify error:unable to get local issuer certificate """) openssl_happy = ("blah.pem", """ blah.pem: good This Update: Dec 20 18:00:00 2016 GMT Next Update: Dec 27 18:00:00 2016 GMT """, "Response verify OK") openssl_revoked = ("blah.pem", """ blah.pem: revoked This Update: Dec 20 01:00:00 2016 GMT Next Update: Dec 27 01:00:00 2016 GMT Revocation Time: Dec 20 01:46:34 2016 GMT """, """Response verify OK""") openssl_unknown = ("blah.pem", """ blah.pem: unknown This Update: Dec 20 18:00:00 2016 GMT Next Update: Dec 27 18:00:00 2016 GMT """, "Response verify OK") openssl_broken = ("", "tentacles", "Response verify OK") openssl_expired_ocsp = ("blah.pem", """ blah.pem: WARNING: Status times invalid. 140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372: good This Update: Apr 6 00:00:00 2016 GMT Next Update: Apr 13 00:00:00 2016 GMT """, """Response verify OK""") openssl_expired_ocsp_revoked = ("blah.pem", """ blah.pem: WARNING: Status times invalid. 140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372: revoked This Update: Apr 6 00:00:00 2016 GMT Next Update: Apr 13 00:00:00 2016 GMT """, """Response verify OK""") if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3350835 certbot-2.9.0/certbot/_internal/tests/plugins/0000775000175100017510000000000014561227516020431 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/__init__.py0000664000175100017510000000003414561227515022536 0ustar00ericaerica"""Certbot Plugins Tests""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/common_test.py0000664000175100017510000003415614561227515023342 0ustar00ericaerica"""Tests for certbot.plugins.common.""" import functools import shutil import sys import unittest from unittest import mock import josepy as jose import pytest from acme import challenges from acme import messages from certbot import achallenges from certbot import crypto_util from certbot import errors from certbot.compat import filesystem from certbot.compat import os from certbot.tests import acme_util from certbot.tests import util as test_util AUTH_KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) ACHALL = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb(challenges.HTTP01(token=b'token1'), messages.STATUS_PENDING), domain="encryption-example.demo", account_key=AUTH_KEY) class NamespaceFunctionsTest(unittest.TestCase): """Tests for certbot.plugins.common.*_namespace functions.""" def test_option_namespace(self): from certbot.plugins.common import option_namespace assert "foo-" == option_namespace("foo") def test_dest_namespace(self): from certbot.plugins.common import dest_namespace assert "foo_" == dest_namespace("foo") def test_dest_namespace_with_dashes(self): from certbot.plugins.common import dest_namespace assert "foo_bar_" == dest_namespace("foo-bar") class PluginTest(unittest.TestCase): """Test for certbot.plugins.common.Plugin.""" def setUp(self): from certbot.plugins.common import Plugin class MockPlugin(Plugin): # pylint: disable=missing-docstring def prepare(self) -> None: pass def more_info(self) -> str: return "info" @classmethod def add_parser_arguments(cls, add): add("foo-bar", dest="different_to_foo_bar", x=1, y=None) self.plugin_cls = MockPlugin self.config = mock.MagicMock() self.plugin = MockPlugin(config=self.config, name="mock") def test_init(self): assert "mock" == self.plugin.name assert self.config == self.plugin.config def test_option_namespace(self): assert "mock-" == self.plugin.option_namespace def test_option_name(self): assert "mock-foo_bar" == self.plugin.option_name("foo_bar") def test_dest_namespace(self): assert "mock_" == self.plugin.dest_namespace def test_dest(self): assert "mock_foo_bar" == self.plugin.dest("foo-bar") assert "mock_foo_bar" == self.plugin.dest("foo_bar") def test_conf(self): assert self.config.mock_foo_bar == self.plugin.conf("foo-bar") def test_inject_parser_options(self): parser = mock.MagicMock() self.plugin_cls.inject_parser_options(parser, "mock") # note that inject_parser_options doesn't check if dest has # correct prefix parser.add_argument.assert_called_once_with( "--mock-foo-bar", dest="different_to_foo_bar", x=1, y=None) def test_fallback_auth_hint(self): assert "the mock plugin completed the required dns-01 challenges" in \ self.plugin.auth_hint([acme_util.DNS01_A, acme_util.DNS01_A]) assert "the mock plugin completed the required dns-01 and http-01 challenges" in \ self.plugin.auth_hint([acme_util.DNS01_A, acme_util.HTTP01_A, acme_util.DNS01_A]) class InstallerTest(test_util.ConfigTestCase): """Tests for certbot.plugins.common.Installer.""" def setUp(self): super().setUp() filesystem.mkdir(self.config.config_dir) from certbot.tests.util import DummyInstaller self.installer = DummyInstaller(config=self.config, name="Installer") self.reverter = self.installer.reverter def test_add_to_real_checkpoint(self): files = {"foo.bar", "baz.qux",} save_notes = "foo bar baz qux" self._test_wrapped_method("add_to_checkpoint", files, save_notes) def test_add_to_real_checkpoint2(self): self._test_add_to_checkpoint_common(False) def test_add_to_temporary_checkpoint(self): self._test_add_to_checkpoint_common(True) def _test_add_to_checkpoint_common(self, temporary): files = {"foo.bar", "baz.qux",} save_notes = "foo bar baz qux" installer_func = functools.partial(self.installer.add_to_checkpoint, temporary=temporary) if temporary: reverter_func_name = "add_to_temp_checkpoint" else: reverter_func_name = "add_to_checkpoint" self._test_adapted_method(installer_func, reverter_func_name, files, save_notes) def test_finalize_checkpoint(self): self._test_wrapped_method("finalize_checkpoint", "foo") def test_recovery_routine(self): self._test_wrapped_method("recovery_routine") def test_revert_temporary_config(self): self._test_wrapped_method("revert_temporary_config") def test_rollback_checkpoints(self): self._test_wrapped_method("rollback_checkpoints", 42) def _test_wrapped_method(self, name, *args, **kwargs): """Test a wrapped reverter method. :param str name: name of the method to test :param tuple args: position arguments to method :param dict kwargs: keyword arguments to method """ installer_func = getattr(self.installer, name) self._test_adapted_method(installer_func, name, *args, **kwargs) def _test_adapted_method(self, installer_func, reverter_func_name, *passed_args, **passed_kwargs): """Test an adapted reverter method :param callable installer_func: installer method to test :param str reverter_func_name: name of the method on the reverter that should be called :param tuple passed_args: positional arguments passed from installer method to the reverter method :param dict passed_kargs: keyword arguments passed from installer method to the reverter method """ with mock.patch.object(self.reverter, reverter_func_name) as reverter_func: installer_func(*passed_args, **passed_kwargs) reverter_func.assert_called_once_with(*passed_args, **passed_kwargs) reverter_func.side_effect = errors.ReverterError with pytest.raises(errors.PluginError): installer_func(*passed_args, **passed_kwargs) def test_install_ssl_dhparams(self): self.installer.install_ssl_dhparams() assert os.path.isfile(self.installer.ssl_dhparams) def _current_ssl_dhparams_hash(self): from certbot._internal.constants import SSL_DHPARAMS_SRC return crypto_util.sha256sum(SSL_DHPARAMS_SRC) def test_current_file_hash_in_all_hashes(self): from certbot._internal.constants import ALL_SSL_DHPARAMS_HASHES assert self._current_ssl_dhparams_hash() in ALL_SSL_DHPARAMS_HASHES, \ "Constants.ALL_SSL_DHPARAMS_HASHES must be appended" \ " with the sha256 hash of self.config.ssl_dhparams when it is updated." class AddrTest(unittest.TestCase): """Tests for certbot.plugins.common.Addr.""" def setUp(self): from certbot.plugins.common import Addr self.addr1 = Addr.fromstring("192.168.1.1") self.addr2 = Addr.fromstring("192.168.1.1:*") self.addr3 = Addr.fromstring("192.168.1.1:80") self.addr4 = Addr.fromstring("[fe00::1]") self.addr5 = Addr.fromstring("[fe00::1]:*") self.addr6 = Addr.fromstring("[fe00::1]:80") self.addr7 = Addr.fromstring("[fe00::1]:5") self.addr8 = Addr.fromstring("[fe00:1:2:3:4:5:6:7:8:9]:8080") def test_fromstring(self): assert self.addr1.get_addr() == "192.168.1.1" assert self.addr1.get_port() == "" assert self.addr2.get_addr() == "192.168.1.1" assert self.addr2.get_port() == "*" assert self.addr3.get_addr() == "192.168.1.1" assert self.addr3.get_port() == "80" assert self.addr4.get_addr() == "[fe00::1]" assert self.addr4.get_port() == "" assert self.addr5.get_addr() == "[fe00::1]" assert self.addr5.get_port() == "*" assert self.addr6.get_addr() == "[fe00::1]" assert self.addr6.get_port() == "80" assert self.addr6.get_ipv6_exploded() == \ "fe00:0:0:0:0:0:0:1" assert self.addr1.get_ipv6_exploded() == \ "" assert self.addr7.get_port() == "5" assert self.addr8.get_ipv6_exploded() == \ "fe00:1:2:3:4:5:6:7" def test_str(self): assert str(self.addr1) == "192.168.1.1" assert str(self.addr2) == "192.168.1.1:*" assert str(self.addr3) == "192.168.1.1:80" assert str(self.addr4) == "[fe00::1]" assert str(self.addr5) == "[fe00::1]:*" assert str(self.addr6) == "[fe00::1]:80" def test_get_addr_obj(self): assert str(self.addr1.get_addr_obj("443")) == "192.168.1.1:443" assert str(self.addr2.get_addr_obj("")) == "192.168.1.1" assert str(self.addr1.get_addr_obj("*")) == "192.168.1.1:*" assert str(self.addr4.get_addr_obj("443")) == "[fe00::1]:443" assert str(self.addr5.get_addr_obj("")) == "[fe00::1]" assert str(self.addr4.get_addr_obj("*")) == "[fe00::1]:*" def test_eq(self): assert self.addr1 == self.addr2.get_addr_obj("") assert self.addr1 != self.addr2 assert self.addr1 != 3333 assert self.addr4 == self.addr4.get_addr_obj("") assert self.addr4 != self.addr5 assert self.addr4 != 3333 from certbot.plugins.common import Addr assert self.addr4 == Addr.fromstring("[fe00:0:0::1]") assert self.addr4 == Addr.fromstring("[fe00:0::0:0:1]") def test_set_inclusion(self): from certbot.plugins.common import Addr set_a = {self.addr1, self.addr2} addr1b = Addr.fromstring("192.168.1.1") addr2b = Addr.fromstring("192.168.1.1:*") set_b = {addr1b, addr2b} assert set_a == set_b set_c = {self.addr4, self.addr5} addr4b = Addr.fromstring("[fe00::1]") addr5b = Addr.fromstring("[fe00::1]:*") set_d = {addr4b, addr5b} assert set_c == set_d class ChallengePerformerTest(unittest.TestCase): """Tests for certbot.plugins.common.ChallengePerformer.""" def setUp(self): configurator = mock.MagicMock() from certbot.plugins.common import ChallengePerformer self.performer = ChallengePerformer(configurator) def test_add_chall(self): self.performer.add_chall(ACHALL, 0) assert 1 == len(self.performer.achalls) assert [0] == self.performer.indices def test_perform(self): with pytest.raises(NotImplementedError): self.performer.perform() class InstallVersionControlledFileTest(test_util.TempDirTestCase): """Tests for certbot.plugins.common.install_version_controlled_file.""" def setUp(self): super().setUp() self.hashes = ["someotherhash"] self.dest_path = os.path.join(self.tempdir, "options-ssl-dest.conf") self.hash_path = os.path.join(self.tempdir, ".options-ssl-conf.txt") self.old_path = os.path.join(self.tempdir, "options-ssl-old.conf") self.source_path = os.path.join(self.tempdir, "options-ssl-src.conf") for path in (self.source_path, self.old_path,): with open(path, "w") as f: f.write(path) self.hashes.append(crypto_util.sha256sum(path)) def _call(self): from certbot.plugins.common import install_version_controlled_file install_version_controlled_file(self.dest_path, self.hash_path, self.source_path, self.hashes) def _current_file_hash(self): return crypto_util.sha256sum(self.source_path) def _assert_current_file(self): assert os.path.isfile(self.dest_path) assert crypto_util.sha256sum(self.dest_path) == \ self._current_file_hash() def test_no_file(self): assert not os.path.isfile(self.dest_path) self._call() self._assert_current_file() def test_current_file(self): # 1st iteration installs the file, the 2nd checks if it needs updating for _ in range(2): self._call() self._assert_current_file() def test_prev_file_updates_to_current(self): shutil.copyfile(self.old_path, self.dest_path) self._call() self._assert_current_file() def test_manually_modified_current_file_does_not_update(self): self._call() with open(self.dest_path, "a") as mod_ssl_conf: mod_ssl_conf.write("a new line for the wrong hash\n") with mock.patch("certbot.plugins.common.logger") as mock_logger: self._call() assert mock_logger.warning.called is False assert os.path.isfile(self.dest_path) assert crypto_util.sha256sum(self.source_path) == \ self._current_file_hash() assert crypto_util.sha256sum(self.dest_path) != \ self._current_file_hash() def test_manually_modified_past_file_warns(self): with open(self.dest_path, "a") as mod_ssl_conf: mod_ssl_conf.write("a new line for the wrong hash\n") with open(self.hash_path, "w") as f: f.write("hashofanoldversion") with mock.patch("certbot.plugins.common.logger") as mock_logger: self._call() assert mock_logger.warning.call_args[0][0] == \ "%s has been manually modified; updated file " \ "saved to %s. We recommend updating %s for security purposes." assert crypto_util.sha256sum(self.source_path) == \ self._current_file_hash() # only print warning once with mock.patch("certbot.plugins.common.logger") as mock_logger: self._call() assert mock_logger.warning.called is False if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/disco_test.py0000664000175100017510000002623214561227515023147 0ustar00ericaerica"""Tests for certbot._internal.plugins.disco.""" import functools import string import sys from typing import List import unittest from unittest import mock import pytest from certbot import errors from certbot import interfaces from certbot._internal.plugins import null from certbot._internal.plugins import standalone from certbot._internal.plugins import webroot if sys.version_info >= (3, 10): # pragma: no cover import importlib.metadata as importlib_metadata else: import importlib_metadata class _EntryPointLoadFail(importlib_metadata.EntryPoint): def load(self): raise RuntimeError("Loading failure") EP_SA = importlib_metadata.EntryPoint( name="sa", value="certbot._internal.plugins.standalone:Authenticator", group="certbot.plugins") EP_WR = importlib_metadata.EntryPoint( name="wr", value="certbot._internal.plugins.webroot:Authenticator", group="certbot.plugins") EP_SA_LOADFAIL = _EntryPointLoadFail( name="sa", value="certbot._internal.plugins.standalone:Authenticator", group="certbot.plugins") class PluginEntryPointTest(unittest.TestCase): """Tests for certbot._internal.plugins.disco.PluginEntryPoint.""" def setUp(self): self.ep1 = importlib_metadata.EntryPoint( name="ep1", value="p1.ep1:Authenticator", group="certbot.plugins") self.ep1prim = importlib_metadata.EntryPoint( name="ep1", value="p2.pe2:Authenticator", group="certbot.plugins") # nested self.ep2 = importlib_metadata.EntryPoint( name="ep2", value="p2.foo.ep2:Authenticator", group="certbot.plugins") # project name != top-level package name self.ep3 = importlib_metadata.EntryPoint( name="ep3", value="a.ep3:Authenticator", group="certbot.plugins") from certbot._internal.plugins.disco import PluginEntryPoint self.plugin_ep = PluginEntryPoint(EP_SA) def test_entry_point_to_plugin_name_not_prefixed(self): from certbot._internal.plugins.disco import PluginEntryPoint names = { self.ep1: "ep1", self.ep1prim: "ep1", self.ep2: "ep2", self.ep3: "ep3", EP_SA: "sa", } for entry_point, name in names.items(): assert name == PluginEntryPoint.entry_point_to_plugin_name(entry_point) def test_description(self): assert "server locally" in self.plugin_ep.description def test_description_with_name(self): self.plugin_ep.plugin_cls = mock.MagicMock(description="Desc") assert "Desc (sa)" == self.plugin_ep.description_with_name def test_long_description(self): self.plugin_ep.plugin_cls = mock.MagicMock( long_description="Long desc") assert "Long desc" == self.plugin_ep.long_description def test_long_description_nonexistent(self): self.plugin_ep.plugin_cls = mock.MagicMock( description="Long desc not found", spec=["description"]) assert "Long desc not found" == self.plugin_ep.long_description def test_ifaces(self): assert self.plugin_ep.ifaces((interfaces.Authenticator,)) assert not self.plugin_ep.ifaces((interfaces.Installer,)) assert not self.plugin_ep.ifaces(( interfaces.Installer, interfaces.Authenticator)) def test__init__(self): assert self.plugin_ep.initialized is False assert self.plugin_ep.prepared is False assert self.plugin_ep.misconfigured is False assert self.plugin_ep.available is False assert self.plugin_ep.problem is None assert self.plugin_ep.entry_point is EP_SA assert "sa" == self.plugin_ep.name assert self.plugin_ep.plugin_cls is standalone.Authenticator def test_init(self): config = mock.MagicMock() plugin = self.plugin_ep.init(config=config) assert self.plugin_ep.initialized is True assert plugin.config is config # memoize! assert self.plugin_ep.init() is plugin assert plugin.config is config # try to give different config assert self.plugin_ep.init(123) is plugin assert plugin.config is config assert self.plugin_ep.prepared is False assert self.plugin_ep.misconfigured is False assert self.plugin_ep.available is False def test_prepare(self): config = mock.MagicMock() self.plugin_ep.init(config=config) self.plugin_ep.prepare() assert self.plugin_ep.prepared assert self.plugin_ep.misconfigured is False # output doesn't matter that much, just test if it runs str(self.plugin_ep) def test_prepare_misconfigured(self): plugin = mock.MagicMock() plugin.prepare.side_effect = errors.MisconfigurationError # pylint: disable=protected-access self.plugin_ep._initialized = plugin assert isinstance(self.plugin_ep.prepare(), errors.MisconfigurationError) assert self.plugin_ep.prepared assert self.plugin_ep.misconfigured assert isinstance(self.plugin_ep.problem, errors.MisconfigurationError) assert self.plugin_ep.available def test_prepare_no_installation(self): plugin = mock.MagicMock() plugin.prepare.side_effect = errors.NoInstallationError # pylint: disable=protected-access self.plugin_ep._initialized = plugin assert isinstance(self.plugin_ep.prepare(), errors.NoInstallationError) assert self.plugin_ep.prepared is True assert self.plugin_ep.misconfigured is False assert self.plugin_ep.available is False def test_prepare_generic_plugin_error(self): plugin = mock.MagicMock() plugin.prepare.side_effect = errors.PluginError # pylint: disable=protected-access self.plugin_ep._initialized = plugin assert isinstance(self.plugin_ep.prepare(), errors.PluginError) assert self.plugin_ep.prepared assert self.plugin_ep.misconfigured is False assert self.plugin_ep.available is False def test_str(self): output = str(self.plugin_ep) assert "Authenticator" in output assert "Installer" not in output assert "Plugin" in output def test_repr(self): assert "PluginEntryPoint#sa" == repr(self.plugin_ep) class PluginsRegistryTest(unittest.TestCase): """Tests for certbot._internal.plugins.disco.PluginsRegistry.""" @classmethod def _create_new_registry(cls, plugins): from certbot._internal.plugins.disco import PluginsRegistry return PluginsRegistry(plugins) def setUp(self): self.plugin_ep = mock.MagicMock() self.plugin_ep.name = "mock" self.plugin_ep.__hash__.side_effect = TypeError self.plugins = {self.plugin_ep.name: self.plugin_ep} self.reg = self._create_new_registry(self.plugins) self.ep1 = importlib_metadata.EntryPoint( name="ep1", value="p1.ep1", group="certbot.plugins") def test_find_all(self): from certbot._internal.plugins.disco import PluginsRegistry with mock.patch("certbot._internal.plugins.disco.importlib_metadata") as mock_meta: mock_meta.entry_points.side_effect = [ [EP_SA], [EP_WR, self.ep1], ] with mock.patch.object(importlib_metadata.EntryPoint, 'load') as mock_load: mock_load.side_effect = [ standalone.Authenticator, webroot.Authenticator, null.Installer, null.Installer] plugins = PluginsRegistry.find_all() assert plugins["sa"].plugin_cls is standalone.Authenticator assert plugins["sa"].entry_point is EP_SA assert plugins["wr"].plugin_cls is webroot.Authenticator assert plugins["wr"].entry_point is EP_WR assert plugins["ep1"].plugin_cls is null.Installer assert plugins["ep1"].entry_point is self.ep1 assert "p1:ep1" not in plugins def test_find_all_error_message(self): from certbot._internal.plugins.disco import PluginsRegistry with mock.patch("certbot._internal.plugins.disco.importlib_metadata") as mock_meta: #EP_SA.load = None # This triggers a TypeError when the entrypoint loads mock_meta.entry_points.side_effect = [ [EP_SA_LOADFAIL], [EP_WR, self.ep1], ] with self.assertRaises(errors.PluginError) as cm: PluginsRegistry.find_all() assert "standalone' plugin errored" in str(cm.exception) def test_getitem(self): assert self.plugin_ep == self.reg["mock"] def test_iter(self): assert ["mock"] == list(self.reg) def test_len(self): assert 0 == len(self._create_new_registry({})) assert 1 == len(self.reg) def test_init(self): self.plugin_ep.init.return_value = "baz" assert ["baz"] == self.reg.init("bar") self.plugin_ep.init.assert_called_once_with("bar") def test_filter(self): assert self.plugins == \ self.reg.filter(lambda p_ep: p_ep.name.startswith("m")) assert {} == self.reg.filter(lambda p_ep: p_ep.name.startswith("b")) def test_ifaces(self): self.plugin_ep.ifaces.return_value = True # pylint: disable=protected-access assert self.plugins == self.reg.ifaces()._plugins self.plugin_ep.ifaces.return_value = False assert {} == self.reg.ifaces()._plugins def test_prepare(self): self.plugin_ep.prepare.return_value = "baz" assert ["baz"] == self.reg.prepare() self.plugin_ep.prepare.assert_called_once_with() def test_prepare_order(self): order: List[str] = [] plugins = { c: mock.MagicMock(prepare=functools.partial(order.append, c)) for c in string.ascii_letters } reg = self._create_new_registry(plugins) reg.prepare() # order of prepare calls must be sorted to prevent deadlock # caused by plugins acquiring locks during prepare assert order == sorted(string.ascii_letters) def test_available(self): self.plugin_ep.available = True # pylint: disable=protected-access assert self.plugins == self.reg.available()._plugins self.plugin_ep.available = False assert {} == self.reg.available()._plugins def test_find_init(self): assert self.reg.find_init(mock.Mock()) is None self.plugin_ep.initialized = True assert self.reg.find_init(self.plugin_ep.init()) is self.plugin_ep def test_repr(self): self.plugin_ep.__repr__ = lambda _: "PluginEntryPoint#mock" assert "PluginsRegistry(PluginEntryPoint#mock)" == \ repr(self.reg) def test_str(self): assert "No plugins" == str(self._create_new_registry({})) self.plugin_ep.__str__ = lambda _: "Mock" assert "Mock" == str(self.reg) plugins = {self.plugin_ep.name: self.plugin_ep, "foo": "Bar"} reg = self._create_new_registry(plugins) assert "Bar\n\nMock" == str(reg) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/dns_common_test.py0000664000175100017510000002051214561227515024175 0ustar00ericaerica"""Tests for certbot.plugins.dns_common.""" import collections import logging import sys import unittest from unittest import mock import pytest from certbot import errors from certbot import util from certbot.compat import os from certbot.display import util as display_util from certbot.plugins import dns_common from certbot.plugins import dns_test_common from certbot.tests import util as test_util class DNSAuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): # pylint: disable=protected-access class _FakeDNSAuthenticator(dns_common.DNSAuthenticator): _setup_credentials = mock.MagicMock() _perform = mock.MagicMock() _cleanup = mock.MagicMock() def more_info(self): # pylint: disable=missing-docstring,no-self-use return 'A fake authenticator for testing.' class _FakeConfig: fake_propagation_seconds = 0 fake_config_key = 1 fake_other_key = None fake_file_path = None def setUp(self): super().setUp() self.config = DNSAuthenticatorTest._FakeConfig() self.auth = DNSAuthenticatorTest._FakeDNSAuthenticator(self.config, "fake") @test_util.patch_display_util() def test_perform(self, unused_mock_get_utility): self.auth.perform([self.achall]) self.auth._perform.assert_called_once_with(dns_test_common.DOMAIN, mock.ANY, mock.ANY) def test_cleanup(self): self.auth._attempt_cleanup = True self.auth.cleanup([self.achall]) self.auth._cleanup.assert_called_once_with(dns_test_common.DOMAIN, mock.ANY, mock.ANY) @test_util.patch_display_util() def test_prompt(self, mock_get_utility): mock_display = mock_get_utility() mock_display.input.side_effect = ((display_util.OK, "",), (display_util.OK, "value",)) self.auth._configure("other_key", "") assert self.auth.config.fake_other_key == "value" @test_util.patch_display_util() def test_prompt_canceled(self, mock_get_utility): mock_display = mock_get_utility() mock_display.input.side_effect = ((display_util.CANCEL, "c",),) with pytest.raises(errors.PluginError): self.auth._configure("other_key", "") @test_util.patch_display_util() def test_prompt_file(self, mock_get_utility): path = os.path.join(self.tempdir, 'file.ini') open(path, "wb").close() mock_display = mock_get_utility() mock_display.directory_select.side_effect = ((display_util.OK, "",), (display_util.OK, "not-a-file.ini",), (display_util.OK, self.tempdir), (display_util.OK, path,)) self.auth._configure_file("file_path", "") assert self.auth.config.fake_file_path == path @test_util.patch_display_util() def test_prompt_file_canceled(self, mock_get_utility): mock_display = mock_get_utility() mock_display.directory_select.side_effect = ((display_util.CANCEL, "c",),) with pytest.raises(errors.PluginError): self.auth._configure_file("file_path", "") def test_configure_credentials(self): path = os.path.join(self.tempdir, 'file.ini') dns_test_common.write({"fake_test": "value"}, path) setattr(self.config, "fake_credentials", path) credentials = self.auth._configure_credentials("credentials", "", {"test": ""}) assert credentials.conf("test") == "value" @test_util.patch_display_util() def test_prompt_credentials(self, mock_get_utility): bad_path = os.path.join(self.tempdir, 'bad-file.ini') dns_test_common.write({"fake_other": "other_value"}, bad_path) path = os.path.join(self.tempdir, 'file.ini') dns_test_common.write({"fake_test": "value"}, path) setattr(self.config, "fake_credentials", "") mock_display = mock_get_utility() mock_display.directory_select.side_effect = ((display_util.OK, "",), (display_util.OK, "not-a-file.ini",), (display_util.OK, self.tempdir), (display_util.OK, bad_path), (display_util.OK, path,)) credentials = self.auth._configure_credentials("credentials", "", {"test": ""}) assert credentials.conf("test") == "value" def test_auth_hint(self): assert 'try increasing --fake-propagation-seconds (currently 0 seconds).' in \ self.auth.auth_hint([mock.MagicMock()]) class CredentialsConfigurationTest(test_util.TempDirTestCase): class _MockLoggingHandler(logging.Handler): messages = None def __init__(self, *args, **kwargs): self.reset() super().__init__(*args, **kwargs) def emit(self, record): self.messages[record.levelname.lower()].append(record.getMessage()) def reset(self): """Allows the handler to be reset between tests.""" self.messages = collections.defaultdict(list) def test_valid_file(self): path = os.path.join(self.tempdir, 'too-permissive-file.ini') dns_test_common.write({"test": "value", "other": 1}, path) credentials_configuration = dns_common.CredentialsConfiguration(path) assert "value" == credentials_configuration.conf("test") assert "1" == credentials_configuration.conf("other") def test_nonexistent_file(self): path = os.path.join(self.tempdir, 'not-a-file.ini') with pytest.raises(errors.PluginError): dns_common.CredentialsConfiguration(path) def test_valid_file_with_unsafe_permissions(self): log = self._MockLoggingHandler() dns_common.logger.addHandler(log) path = os.path.join(self.tempdir, 'too-permissive-file.ini') util.safe_open(path, "wb", 0o744).close() dns_common.CredentialsConfiguration(path) assert 1 == len([_ for _ in log.messages['warning'] if _.startswith("Unsafe")]) class CredentialsConfigurationRequireTest(test_util.TempDirTestCase): def setUp(self): super().setUp() self.path = os.path.join(self.tempdir, 'file.ini') def _write(self, values): dns_test_common.write(values, self.path) def test_valid(self): self._write({"test": "value", "other": 1}) credentials_configuration = dns_common.CredentialsConfiguration(self.path) credentials_configuration.require({"test": "", "other": ""}) def test_valid_but_extra(self): self._write({"test": "value", "other": 1}) credentials_configuration = dns_common.CredentialsConfiguration(self.path) credentials_configuration.require({"test": ""}) def test_valid_empty(self): self._write({}) credentials_configuration = dns_common.CredentialsConfiguration(self.path) credentials_configuration.require({}) def test_missing(self): self._write({}) credentials_configuration = dns_common.CredentialsConfiguration(self.path) with pytest.raises(errors.PluginError): credentials_configuration.require({"test": ""}) def test_blank(self): self._write({"test": ""}) credentials_configuration = dns_common.CredentialsConfiguration(self.path) with pytest.raises(errors.PluginError): credentials_configuration.require({"test": ""}) def test_typo(self): self._write({"tets": "typo!"}) credentials_configuration = dns_common.CredentialsConfiguration(self.path) with pytest.raises(errors.PluginError): credentials_configuration.require({"test": ""}) class DomainNameGuessTest(unittest.TestCase): def test_simple_case(self): assert 'example.com' in \ dns_common.base_domain_name_guesses("example.com") def test_sub_domain(self): assert 'example.com' in \ dns_common.base_domain_name_guesses("foo.bar.baz.example.com") def test_second_level_domain(self): assert 'example.co.uk' in \ dns_common.base_domain_name_guesses("foo.bar.baz.example.co.uk") if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/enhancements_test.py0000664000175100017510000000444414561227515024517 0ustar00ericaerica"""Tests for new style enhancements""" import sys import unittest from unittest import mock import pytest from certbot._internal.plugins import null from certbot.plugins import enhancements import certbot.tests.util as test_util class EnhancementTest(test_util.ConfigTestCase): """Tests for new style enhancements in certbot.plugins.enhancements""" def setUp(self): super().setUp() self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement) @test_util.patch_display_util() def test_enhancement_enabled_enhancements(self, _): FAKEINDEX = [ { "name": "autohsts", "cli_dest": "auto_hsts", }, { "name": "somethingelse", "cli_dest": "something", } ] with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX): self.config.auto_hsts = True self.config.something = True enabled = list(enhancements.enabled_enhancements(self.config)) assert len(enabled) == 2 assert [i for i in enabled if i["name"] == "autohsts"] assert [i for i in enabled if i["name"] == "somethingelse"] def test_are_requested(self): assert len(list(enhancements.enabled_enhancements(self.config))) == 0 assert not enhancements.are_requested(self.config) self.config.auto_hsts = True assert len(list(enhancements.enabled_enhancements(self.config))) == 1 assert enhancements.are_requested(self.config) def test_are_supported(self): self.config.auto_hsts = True unsupported = null.Installer(self.config, "null") assert enhancements.are_supported(self.config, self.mockinstaller) assert not enhancements.are_supported(self.config, unsupported) def test_enable(self): self.config.auto_hsts = True domains = ["example.com", "www.example.com"] lineage = "lineage" enhancements.enable(lineage, domains, self.mockinstaller, self.config) assert self.mockinstaller.enable_autohsts.called assert self.mockinstaller.enable_autohsts.call_args[0] == \ (lineage, domains) if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/manual_test.py0000664000175100017510000001640114561227515023320 0ustar00ericaerica"""Tests for certbot._internal.plugins.manual""" import sys import textwrap import unittest from unittest import mock import pytest from acme import challenges from certbot import errors from certbot.compat import filesystem from certbot.compat import os from certbot.tests import acme_util from certbot.tests import util as test_util class AuthenticatorTest(test_util.TempDirTestCase): """Tests for certbot._internal.plugins.manual.Authenticator.""" def setUp(self): super().setUp() get_display_patch = test_util.patch_display_util() self.mock_get_display = get_display_patch.start() self.addCleanup(get_display_patch.stop) self.http_achall = acme_util.HTTP01_A self.dns_achall = acme_util.DNS01_A self.dns_achall_2 = acme_util.DNS01_A_2 self.achalls = [self.http_achall, self.dns_achall, self.dns_achall_2] for d in ["config_dir", "work_dir", "in_progress"]: filesystem.mkdir(os.path.join(self.tempdir, d)) # "backup_dir" and "temp_checkpoint_dir" get created in # certbot.util.make_or_verify_dir() during the Reverter # initialization. self.config = mock.MagicMock( http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None, noninteractive_mode=False, validate_hooks=False, config_dir=os.path.join(self.tempdir, "config_dir"), work_dir=os.path.join(self.tempdir, "work_dir"), backup_dir=os.path.join(self.tempdir, "backup_dir"), temp_checkpoint_dir=os.path.join( self.tempdir, "temp_checkpoint_dir"), in_progress_dir=os.path.join(self.tempdir, "in_progess")) from certbot._internal.plugins.manual import Authenticator self.auth = Authenticator(self.config, name='manual') def test_prepare_no_hook_noninteractive(self): self.config.noninteractive_mode = True with pytest.raises(errors.PluginError): self.auth.prepare() def test_prepare_bad_hook(self): self.config.manual_auth_hook = os.path.abspath(os.sep) # is / on UNIX self.config.validate_hooks = True with pytest.raises(errors.HookCommandNotFound): self.auth.prepare() def test_more_info(self): assert isinstance(self.auth.more_info(), str) def test_get_chall_pref(self): assert self.auth.get_chall_pref('example.org') == \ [challenges.HTTP01, challenges.DNS01] def test_script_perform(self): self.config.manual_auth_hook = ( '{0} -c "' 'from certbot.compat import os;' 'print(os.environ.get(\'CERTBOT_DOMAIN\'));' 'print(os.environ.get(\'CERTBOT_TOKEN\', \'notoken\'));' 'print(os.environ.get(\'CERTBOT_VALIDATION\', \'novalidation\'));' 'print(os.environ.get(\'CERTBOT_ALL_DOMAINS\'));' 'print(os.environ.get(\'CERTBOT_REMAINING_CHALLENGES\'));"' .format(sys.executable)) dns_expected = '{0}\n{1}\n{2}\n{3}\n{4}'.format( self.dns_achall.domain, 'notoken', self.dns_achall.validation(self.dns_achall.account_key), ','.join(achall.domain for achall in self.achalls), len(self.achalls) - self.achalls.index(self.dns_achall) - 1) http_expected = '{0}\n{1}\n{2}\n{3}\n{4}'.format( self.http_achall.domain, self.http_achall.chall.encode('token'), self.http_achall.validation(self.http_achall.account_key), ','.join(achall.domain for achall in self.achalls), len(self.achalls) - self.achalls.index(self.http_achall) - 1) assert self.auth.perform(self.achalls) == \ [achall.response(achall.account_key) for achall in self.achalls] assert self.auth.env[self.dns_achall]['CERTBOT_AUTH_OUTPUT'] == \ dns_expected assert self.auth.env[self.http_achall]['CERTBOT_AUTH_OUTPUT'] == \ http_expected # Successful hook output should be sent to notify assert self.mock_get_display().notification.call_count == len(self.achalls) for i, (args, _) in enumerate(self.mock_get_display().notification.call_args_list): needle = textwrap.indent(self.auth.env[self.achalls[i]]['CERTBOT_AUTH_OUTPUT'], ' ') assert needle in args[0] def test_manual_perform(self): assert self.auth.perform(self.achalls) == \ [achall.response(achall.account_key) for achall in self.achalls] assert self.mock_get_display().notification.call_count == len(self.achalls) for i, (args, kwargs) in enumerate(self.mock_get_display().notification.call_args_list): achall = self.achalls[i] assert achall.validation(achall.account_key) in args[0] assert kwargs['wrap'] is False def test_cleanup(self): self.config.manual_auth_hook = ('{0} -c "import sys; sys.stdout.write(\'foo\')"' .format(sys.executable)) self.config.manual_cleanup_hook = '# cleanup' self.auth.perform(self.achalls) for achall in self.achalls: self.auth.cleanup([achall]) assert os.environ['CERTBOT_AUTH_OUTPUT'] == 'foo' assert os.environ['CERTBOT_DOMAIN'] == achall.domain if isinstance(achall.chall, (challenges.HTTP01, challenges.DNS01)): assert os.environ['CERTBOT_VALIDATION'] == \ achall.validation(achall.account_key) if isinstance(achall.chall, challenges.HTTP01): assert os.environ['CERTBOT_TOKEN'] == \ achall.chall.encode('token') else: assert 'CERTBOT_TOKEN' not in os.environ def test_auth_hint_hook(self): self.config.manual_auth_hook = '/bin/true' assert self.auth.auth_hint([acme_util.DNS01_A, acme_util.HTTP01_A]) == \ 'The Certificate Authority failed to verify the DNS TXT records and challenge ' \ 'files created by the --manual-auth-hook. Ensure that this hook is functioning ' \ 'correctly and that it waits a sufficient duration of time for DNS propagation. ' \ 'Refer to "certbot --help manual" and the Certbot User Guide.' assert self.auth.auth_hint([acme_util.HTTP01_A]) == \ 'The Certificate Authority failed to verify the challenge files created by the ' \ '--manual-auth-hook. Ensure that this hook is functioning correctly. Refer to ' \ '"certbot --help manual" and the Certbot User Guide.' def test_auth_hint_no_hook(self): assert self.auth.auth_hint([acme_util.DNS01_A, acme_util.HTTP01_A]) == \ 'The Certificate Authority failed to verify the manually created DNS TXT records ' \ 'and challenge files. Ensure that you created these in the correct location, or ' \ 'try waiting longer for DNS propagation on the next attempt.' assert self.auth.auth_hint([acme_util.HTTP01_A, acme_util.HTTP01_A, acme_util.HTTP01_A]) == \ 'The Certificate Authority failed to verify the manually created challenge files. ' \ 'Ensure that you created these in the correct location.' if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/null_test.py0000664000175100017510000000124214561227515023012 0ustar00ericaerica"""Tests for certbot._internal.plugins.null.""" import sys import unittest from unittest import mock import pytest class InstallerTest(unittest.TestCase): """Tests for certbot._internal.plugins.null.Installer.""" def setUp(self): from certbot._internal.plugins.null import Installer self.installer = Installer(config=mock.MagicMock(), name="null") def test_it(self): assert isinstance(self.installer.more_info(), str) assert [] == self.installer.get_all_names() assert [] == self.installer.supported_enhancements() if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/selection_test.py0000664000175100017510000002334614561227515024036 0ustar00ericaerica"""Tests for letsencrypt.plugins.selection""" import sys from typing import List import unittest from unittest import mock import pytest from certbot import errors from certbot import interfaces from certbot._internal.display import obj as display_obj from certbot._internal.plugins.disco import PluginsRegistry from certbot.display import util as display_util from certbot.tests import util as test_util class ConveniencePickPluginTest(unittest.TestCase): """Tests for certbot._internal.plugins.selection.pick_*.""" def _test(self, fun, ifaces): config = mock.Mock() default = mock.Mock() plugins = mock.Mock() with mock.patch("certbot._internal.plugins.selection.pick_plugin") as mock_p: mock_p.return_value = "foo" assert "foo" == fun(config, default, plugins, "Question?") mock_p.assert_called_once_with( config, default, plugins, "Question?", ifaces) def test_authenticator(self): from certbot._internal.plugins.selection import pick_authenticator self._test(pick_authenticator, (interfaces.Authenticator,)) def test_installer(self): from certbot._internal.plugins.selection import pick_installer self._test(pick_installer, (interfaces.Installer,)) def test_configurator(self): from certbot._internal.plugins.selection import pick_configurator self._test(pick_configurator, (interfaces.Authenticator, interfaces.Installer)) class PickPluginTest(unittest.TestCase): """Tests for certbot._internal.plugins.selection.pick_plugin.""" def setUp(self): self.config = mock.Mock(noninteractive_mode=False) self.default = None self.reg = mock.MagicMock() self.question = "Question?" self.ifaces: List[interfaces.Plugin] = [] def _call(self): from certbot._internal.plugins.selection import pick_plugin return pick_plugin(self.config, self.default, self.reg, self.question, self.ifaces) def test_default_provided(self): self.default = "foo" self._call() assert 1 == self.reg.filter.call_count def test_no_default(self): self._call() assert 1 == self.reg.visible().ifaces.call_count def test_no_candidate(self): assert self._call() is None def test_single(self): plugin_ep = mock.MagicMock() plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = False self.reg.visible().ifaces().available.return_value = { "bar": plugin_ep} assert "foo" == self._call() def test_single_misconfigured(self): plugin_ep = mock.MagicMock() plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = True self.reg.visible().ifaces().available.return_value = { "bar": plugin_ep} assert self._call() is None def test_multiple(self): plugin_ep = mock.MagicMock() plugin_ep.init.return_value = "foo" self.reg.visible().ifaces().available.return_value = { "bar": plugin_ep, "baz": plugin_ep, } with mock.patch("certbot._internal.plugins.selection.choose_plugin") as mock_choose: mock_choose.return_value = plugin_ep assert "foo" == self._call() mock_choose.assert_called_once_with( [plugin_ep, plugin_ep], self.question) def test_choose_plugin_none(self): self.reg.visible().ifaces().available.return_value = { "bar": None, "baz": None, } with mock.patch("certbot._internal.plugins.selection.choose_plugin") as mock_choose: mock_choose.return_value = None assert self._call() is None def test_default_must_be_filtered(self): # https://github.com/certbot/certbot/issues/9664 self.default = "foo" filtered = mock.MagicMock() self.reg.filter.return_value = filtered self._call() assert filtered.ifaces.call_count == 1 class ChoosePluginTest(unittest.TestCase): """Tests for certbot._internal.plugins.selection.choose_plugin.""" def setUp(self): display_obj.set_display(display_obj.FileDisplay(sys.stdout, False)) self.mock_apache = mock.Mock( description_with_name="a", misconfigured=True) self.mock_apache.name = "apache" self.mock_stand = mock.Mock( description_with_name="s", misconfigured=False) self.mock_stand.init().more_info.return_value = "standalone" self.plugins = [ self.mock_apache, self.mock_stand, ] def _call(self): from certbot._internal.plugins.selection import choose_plugin return choose_plugin(self.plugins, "Question?") @test_util.patch_display_util() def test_selection(self, mock_util): mock_util().menu.side_effect = [(display_util.OK, 0), (display_util.OK, 1)] assert self.mock_stand == self._call() assert mock_util().notification.call_count == 1 @test_util.patch_display_util() def test_more_info(self, mock_util): mock_util().menu.side_effect = [ (display_util.OK, 1), ] assert self.mock_stand == self._call() @test_util.patch_display_util() def test_no_choice(self, mock_util): mock_util().menu.return_value = (display_util.CANCEL, 0) assert self._call() is None class GetUnpreparedInstallerTest(test_util.ConfigTestCase): """Tests for certbot._internal.plugins.selection.get_unprepared_installer.""" def setUp(self): super().setUp() self.mock_apache_fail_ep = mock.Mock( description_with_name="afail") self.mock_apache_fail_ep.check_name = lambda name: name == "afail" self.mock_apache_ep = mock.Mock( description_with_name="apache") self.mock_apache_ep.check_name = lambda name: name == "apache" self.mock_apache_plugin = mock.MagicMock() self.mock_apache_ep.init.return_value = self.mock_apache_plugin self.plugins = PluginsRegistry({ "afail": self.mock_apache_fail_ep, "apache": self.mock_apache_ep, }) def _call(self): from certbot._internal.plugins.selection import get_unprepared_installer return get_unprepared_installer(self.config, self.plugins) def test_no_installer_defined(self): self.config.configurator = None assert self._call() is None def test_no_available_installers(self): self.config.configurator = "apache" self.plugins = PluginsRegistry({}) with pytest.raises(errors.PluginSelectionError): self._call() def test_get_plugin(self): self.config.configurator = "apache" installer = self._call() assert installer is self.mock_apache_plugin def test_multiple_installers_returned(self): self.config.configurator = "apache" # Two plugins with the same name self.mock_apache_fail_ep.check_name = lambda name: name == "apache" with pytest.raises(errors.PluginSelectionError): self._call() class TestChooseConfiguratorPlugins(unittest.TestCase): """Tests for certbot._internal.plugins.selection.choose_configurator_plugins.""" def _setupMockPlugin(self, name): mock_ep = mock.Mock( description_with_name=name) mock_ep.check_name = lambda n: n == name mock_plugin = mock.MagicMock() mock_plugin.name = name mock_ep.init.return_value = mock_plugin mock_ep.misconfigured = False return mock_ep def _parseArgs(self, args): from certbot import configuration from certbot._internal import cli return cli.prepare_and_parse_args(self.plugins, args.split()) def setUp(self): self.plugins = PluginsRegistry({ "nginx": self._setupMockPlugin("nginx"), "apache": self._setupMockPlugin("apache"), "manual": self._setupMockPlugin("manual"), }) def _runWithArgs(self, args): from certbot._internal.plugins.selection import choose_configurator_plugins return choose_configurator_plugins(self._parseArgs(args), self.plugins, "certonly") def test_noninteractive_configurator(self): # For certonly, setting either the nginx or apache configurators should # return both an installer and authenticator inst, auth = self._runWithArgs("certonly --nginx") assert inst.name == "nginx" assert auth.name == "nginx" inst, auth = self._runWithArgs("certonly --apache") assert inst.name == "apache" assert auth.name == "apache" def test_noninteractive_inst_arg(self): # For certonly, if an installer arg is set, it should be returned as expected inst, auth = self._runWithArgs("certonly -a nginx -i nginx") assert inst.name == "nginx" assert auth.name == "nginx" inst, auth = self._runWithArgs("certonly -a apache -i apache") assert inst.name == "apache" assert auth.name == "apache" # if no installer arg is set (or it's set to none), one shouldn't be returned inst, auth = self._runWithArgs("certonly -a nginx") assert inst == None assert auth.name == "nginx" inst, auth = self._runWithArgs("certonly -a nginx -i none") assert inst == None assert auth.name == "nginx" inst, auth = self._runWithArgs("certonly -a apache") assert inst == None assert auth.name == "apache" inst, auth = self._runWithArgs("certonly -a apache -i none") assert inst == None assert auth.name == "apache" if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/standalone_test.py0000664000175100017510000001524114561227515024174 0ustar00ericaerica"""Tests for certbot._internal.plugins.standalone.""" import errno import socket import sys from typing import Dict from typing import Set from typing import Tuple import unittest from unittest import mock import josepy as jose import OpenSSL.crypto import pytest from acme import challenges from acme import standalone as acme_standalone from certbot import achallenges from certbot import errors from certbot.tests import acme_util from certbot.tests import util as test_util class ServerManagerTest(unittest.TestCase): """Tests for certbot._internal.plugins.standalone.ServerManager.""" def setUp(self): from certbot._internal.plugins.standalone import ServerManager self.certs: Dict[bytes, Tuple[OpenSSL.crypto.PKey, OpenSSL.crypto.X509]] = {} self.http_01_resources: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] = {} self.mgr = ServerManager(self.certs, self.http_01_resources) def test_init(self): assert self.mgr.certs is self.certs assert self.mgr.http_01_resources is self.http_01_resources def _test_run_stop(self, challenge_type): server = self.mgr.run(port=0, challenge_type=challenge_type) port = server.getsocknames()[0][1] assert self.mgr.running() == {port: server} self.mgr.stop(port=port) assert self.mgr.running() == {} def test_run_stop_http_01(self): self._test_run_stop(challenges.HTTP01) def test_run_idempotent(self): server = self.mgr.run(port=0, challenge_type=challenges.HTTP01) port = server.getsocknames()[0][1] server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01) assert self.mgr.running() == {port: server} assert server is server2 self.mgr.stop(port) assert self.mgr.running() == {} def test_run_bind_error(self): some_server = socket.socket(socket.AF_INET6) some_server.bind(("", 0)) port = some_server.getsockname()[1] maybe_another_server = socket.socket() try: maybe_another_server.bind(("", port)) except socket.error: pass with pytest.raises(errors.StandaloneBindError): self.mgr.run(port, challenge_type=challenges.HTTP01) assert self.mgr.running() == {} some_server.close() maybe_another_server.close() def get_open_port(): """Gets an open port number from the OS.""" open_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) open_socket.bind(("", 0)) port = open_socket.getsockname()[1] open_socket.close() return port class AuthenticatorTest(unittest.TestCase): """Tests for certbot._internal.plugins.standalone.Authenticator.""" def setUp(self): from certbot._internal.plugins.standalone import Authenticator self.config = mock.MagicMock(http01_port=get_open_port()) self.auth = Authenticator(self.config, name="standalone") self.auth.servers = mock.MagicMock() def test_more_info(self): assert isinstance(self.auth.more_info(), str) def test_get_chall_pref(self): assert self.auth.get_chall_pref(domain=None) == \ [challenges.HTTP01] def test_perform(self): achalls = self._get_achalls() response = self.auth.perform(achalls) expected = [achall.response(achall.account_key) for achall in achalls] assert response == expected @test_util.patch_display_util() def test_perform_eaddrinuse_retry(self, mock_get_utility): mock_utility = mock_get_utility() encountered_errno = errno.EADDRINUSE error = errors.StandaloneBindError(mock.MagicMock(errno=encountered_errno), -1) self.auth.servers.run.side_effect = [error] + 2 * [mock.MagicMock()] mock_yesno = mock_utility.yesno mock_yesno.return_value = True self.test_perform() self._assert_correct_yesno_call(mock_yesno) @test_util.patch_display_util() def test_perform_eaddrinuse_no_retry(self, mock_get_utility): mock_utility = mock_get_utility() mock_yesno = mock_utility.yesno mock_yesno.return_value = False encountered_errno = errno.EADDRINUSE with pytest.raises(errors.PluginError): self._fail_perform(encountered_errno) self._assert_correct_yesno_call(mock_yesno) def _assert_correct_yesno_call(self, mock_yesno): yesno_args, yesno_kwargs = mock_yesno.call_args assert "in use" in yesno_args[0] assert not yesno_kwargs.get("default", True) def test_perform_eacces(self): encountered_errno = errno.EACCES with pytest.raises(errors.PluginError): self._fail_perform(encountered_errno) def test_perform_unexpected_socket_error(self): encountered_errno = errno.ENOTCONN with pytest.raises(errors.StandaloneBindError): self._fail_perform(encountered_errno) def _fail_perform(self, encountered_errno): error = errors.StandaloneBindError(mock.MagicMock(errno=encountered_errno), -1) self.auth.servers.run.side_effect = error self.auth.perform(self._get_achalls()) @classmethod def _get_achalls(cls): domain = b'localhost' key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) http_01 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain=domain, account_key=key) return [http_01] def test_cleanup(self): self.auth.servers.running.return_value = { 1: "server1", 2: "server2", } self.auth.served["server1"].add("chall1") self.auth.served["server2"].update(["chall2", "chall3"]) self.auth.cleanup(["chall1"]) assert self.auth.served == { "server1": set(), "server2": {"chall2", "chall3"}} self.auth.servers.stop.assert_called_once_with(1) self.auth.servers.running.return_value = { 2: "server2", } self.auth.cleanup(["chall2"]) assert self.auth.served == { "server1": set(), "server2": {"chall3"}} assert 1 == self.auth.servers.stop.call_count self.auth.cleanup(["chall3"]) assert self.auth.served == { "server1": set(), "server2": set()} self.auth.servers.stop.assert_called_with(2) def test_auth_hint(self): self.config.http01_port = "80" self.config.http01_address = None assert "on port 80" in self.auth.auth_hint([]) self.config.http01_address = "127.0.0.1" assert "on 127.0.0.1:80" in self.auth.auth_hint([]) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/storage_test.py0000664000175100017510000001264514561227515023515 0ustar00ericaerica"""Tests for certbot.plugins.storage.PluginStorage""" import json import sys from typing import Iterable from typing import List from typing import Optional import unittest from unittest import mock import pytest from certbot import errors from certbot.compat import filesystem from certbot.compat import os from certbot.tests import util as test_util class PluginStorageTest(test_util.ConfigTestCase): """Test for certbot.plugins.storage.PluginStorage""" def setUp(self): super().setUp() self.plugin_cls = test_util.DummyInstaller filesystem.mkdir(self.config.config_dir) with mock.patch("certbot.reverter.util"): self.plugin = self.plugin_cls(config=self.config, name="mockplugin") def test_load_errors_cant_read(self): with open(os.path.join(self.config.config_dir, ".pluginstorage.json"), "w") as fh: fh.write("dummy") # When unable to read file that exists mock_open = mock.mock_open() mock_open.side_effect = IOError self.plugin.storage._storagepath = os.path.join(self.config.config_dir, ".pluginstorage.json") with mock.patch("builtins.open", mock_open): with mock.patch('certbot.compat.os.path.isfile', return_value=True): with mock.patch("certbot.reverter.util"): with pytest.raises(errors.PluginStorageError): self.plugin.storage._load() # pylint: disable=protected-access def test_load_errors_empty(self): with open(os.path.join(self.config.config_dir, ".pluginstorage.json"), "w") as fh: fh.write('') with mock.patch("certbot.plugins.storage.logger.debug") as mock_log: # Should not error out but write a debug log line instead with mock.patch("certbot.reverter.util"): nocontent = self.plugin_cls(self.config, "mockplugin") with pytest.raises(KeyError): nocontent.storage.fetch("value") assert mock_log.called assert "no values loaded" in mock_log.call_args[0][0] def test_load_errors_corrupted(self): with open(os.path.join(self.config.config_dir, ".pluginstorage.json"), "w") as fh: fh.write('invalid json') with mock.patch("certbot.plugins.storage.logger.error") as mock_log: with mock.patch("certbot.reverter.util"): corrupted = self.plugin_cls(self.config, "mockplugin") with pytest.raises(errors.PluginError): corrupted.storage.fetch("value") assert "is corrupted" in mock_log.call_args[0][0] def test_save_errors_cant_serialize(self): with mock.patch("certbot.plugins.storage.logger.error") as mock_log: # Set data as something that can't be serialized self.plugin.storage._initialized = True # pylint: disable=protected-access self.plugin.storage._storagepath = "/tmp/whatever" self.plugin.storage._data = self.plugin_cls # pylint: disable=protected-access with pytest.raises(errors.PluginStorageError): self.plugin.storage.save() assert "Could not serialize" in mock_log.call_args[0][0] def test_save_errors_unable_to_write_file(self): mock_open = mock.mock_open() mock_open.side_effect = IOError with mock.patch("certbot.compat.filesystem.open", mock_open): with mock.patch("certbot.plugins.storage.logger.error") as mock_log: self.plugin.storage._data = {"valid": "data"} # pylint: disable=protected-access self.plugin.storage._initialized = True # pylint: disable=protected-access self.plugin.storage._storagepath = "/tmp/whatever" with pytest.raises(errors.PluginStorageError): self.plugin.storage.save() assert "Could not write" in mock_log.call_args[0][0] def test_save_uninitialized(self): with mock.patch("certbot.reverter.util"): with pytest.raises(errors.PluginStorageError): self.plugin_cls(self.config, "x").storage.save() def test_namespace_isolation(self): with mock.patch("certbot.reverter.util"): plugin1 = self.plugin_cls(self.config, "first") plugin2 = self.plugin_cls(self.config, "second") plugin1.storage.put("first_key", "first_value") with pytest.raises(KeyError): plugin2.storage.fetch("first_key") with pytest.raises(KeyError): plugin2.storage.fetch("first") assert plugin1.storage.fetch("first_key") == "first_value" def test_saved_state(self): self.plugin.storage.put("testkey", "testvalue") # Write to disk self.plugin.storage.save() with mock.patch("certbot.reverter.util"): another = self.plugin_cls(self.config, "mockplugin") assert another.storage.fetch("testkey") == "testvalue" with open(os.path.join(self.config.config_dir, ".pluginstorage.json"), 'r') as fh: psdata = fh.read() psjson = json.loads(psdata) assert "mockplugin" in psjson.keys() assert len(psjson) == 1 assert psjson["mockplugin"]["testkey"] == "testvalue" if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/util_test.py0000664000175100017510000000274714561227515023030 0ustar00ericaerica"""Tests for certbot.plugins.util.""" import sys from unittest import mock import pytest from certbot.compat import os def test_get_prefix(): from certbot.plugins.util import get_prefixes assert get_prefixes('/a/b/c') == \ [os.path.normpath(path) for path in ['/a/b/c', '/a/b', '/a', '/']] assert get_prefixes('/') == [os.path.normpath('/')] assert get_prefixes('a') == ['a'] @mock.patch("certbot.plugins.util.logger.debug") def test_path_surgery(mock_debug): from certbot.plugins.util import path_surgery all_path = {"PATH": "/usr/local/bin:/bin/:/usr/sbin/:/usr/local/sbin/"} with mock.patch.dict('os.environ', all_path): with mock.patch('certbot.util.exe_exists') as mock_exists: mock_exists.return_value = True assert path_surgery("eg") is True assert mock_debug.call_count == 0 assert os.environ["PATH"] == all_path["PATH"] if os.name != 'nt': # This part is specific to Linux since on Windows no PATH surgery is ever done. no_path = {"PATH": "/tmp/"} with mock.patch.dict('os.environ', no_path): path_surgery("thingy") assert mock_debug.call_count == (2 if os.name != 'nt' else 1) assert "Failed to find" in mock_debug.call_args[0][0] assert "/usr/local/bin" in os.environ["PATH"] assert "/tmp" in os.environ["PATH"] if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/plugins/webroot_test.py0000664000175100017510000003603214561227515023526 0ustar00ericaerica"""Tests for certbot._internal.plugins.webroot.""" from __future__ import print_function import argparse import errno import json import shutil import sys import tempfile import unittest from unittest import mock import josepy as jose import pytest from acme import challenges from certbot import achallenges from certbot import errors from certbot.compat import filesystem from certbot.compat import os from certbot.display import util as display_util from certbot.tests import acme_util from certbot.tests import util as test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class AuthenticatorTest(unittest.TestCase): """Tests for certbot._internal.plugins.webroot.Authenticator.""" achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="thing.com", account_key=KEY) def setUp(self): from certbot._internal.plugins.webroot import Authenticator # On Linux directories created by tempfile.mkdtemp inherit their permissions from their # parent directory. So the actual permissions are inconsistent over various tests env. # To circumvent this, a dedicated sub-workspace is created under the workspace, using # filesystem.mkdir to get consistent permissions. self.workspace = tempfile.mkdtemp() self.path = os.path.join(self.workspace, 'webroot') filesystem.mkdir(self.path) self.partial_root_challenge_path = os.path.join( self.path, ".well-known") self.root_challenge_path = os.path.join( self.path, ".well-known", "acme-challenge") self.validation_path = os.path.join( self.root_challenge_path, "ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ") self.config = mock.MagicMock(webroot_path=self.path, webroot_map={"thing.com": self.path}) self.auth = Authenticator(self.config, "webroot") def tearDown(self): shutil.rmtree(self.path) def test_more_info(self): more_info = self.auth.more_info() assert isinstance(more_info, str) assert self.path in more_info def test_add_parser_arguments(self): add = mock.MagicMock() self.auth.add_parser_arguments(add) assert 2 == add.call_count def test_prepare(self): self.auth.prepare() # shouldn't raise any exceptions @test_util.patch_display_util() def test_webroot_from_list(self, mock_get_utility): self.config.webroot_path = [] self.config.webroot_map = {"otherthing.com": self.path} mock_display = mock_get_utility() mock_display.menu.return_value = (display_util.OK, 1,) self.auth.perform([self.achall]) assert mock_display.menu.called for call in mock_display.menu.call_args_list: assert self.achall.domain in call[0][0] assert all( webroot in call[0][1] for webroot in self.config.webroot_map.values()) assert self.config.webroot_map[self.achall.domain] == \ self.path @unittest.skipIf(filesystem.POSIX_MODE, reason='Test specific to Windows') @test_util.patch_display_util() def test_webconfig_file_generate_and_cleanup(self, mock_get_utility): mock_display = mock_get_utility() mock_display.menu.return_value = (display_util.OK, 1,) self.auth.perform([self.achall]) assert os.path.exists(os.path.join(self.root_challenge_path, "web.config")) self.auth.cleanup([self.achall]) assert not os.path.exists(os.path.join(self.root_challenge_path, "web.config")) @unittest.skipIf(filesystem.POSIX_MODE, reason='Test specific to Windows') @test_util.patch_display_util() def test_foreign_webconfig_file_handling(self, mock_get_utility): mock_display = mock_get_utility() mock_display.menu.return_value = (display_util.OK, 1,) challenge_path = os.path.join(self.path, ".well-known", "acme-challenge") filesystem.makedirs(challenge_path) webconfig_path = os.path.join(challenge_path, "web.config") with open(webconfig_path, "w") as file: file.write("something") self.auth.perform([self.achall]) from certbot import crypto_util webconfig_hash = crypto_util.sha256sum(webconfig_path) from certbot._internal.plugins.webroot import _WEB_CONFIG_SHA256SUMS assert webconfig_hash not in _WEB_CONFIG_SHA256SUMS @unittest.skipIf(filesystem.POSIX_MODE, reason='Test specific to Windows') def test_foreign_webconfig_multiple_domains(self): # Covers bug https://github.com/certbot/certbot/issues/9091 achall_2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb(challenges.HTTP01(token=b"bingo"), "pending"), domain="second-thing.com", account_key=KEY) self.config.webroot_map["second-thing.com"] = self.path challenge_path = os.path.join(self.path, ".well-known", "acme-challenge") filesystem.makedirs(challenge_path) webconfig_path = os.path.join(challenge_path, "web.config") with open(webconfig_path, "w") as file: file.write("something") self.auth.perform([self.achall, achall_2]) @test_util.patch_display_util() def test_webroot_from_list_help_and_cancel(self, mock_get_utility): self.config.webroot_path = [] self.config.webroot_map = {"otherthing.com": self.path} mock_display = mock_get_utility() mock_display.menu.side_effect = ((display_util.CANCEL, -1),) with pytest.raises(errors.PluginError): self.auth.perform([self.achall]) assert mock_display.menu.called for call in mock_display.menu.call_args_list: assert self.achall.domain in call[0][0] assert all( webroot in call[0][1] for webroot in self.config.webroot_map.values()) @test_util.patch_display_util() def test_new_webroot(self, mock_get_utility): self.config.webroot_path = [] self.config.webroot_map = {"something.com": self.path} mock_display = mock_get_utility() mock_display.menu.return_value = (display_util.OK, 0,) with mock.patch('certbot.display.ops.validated_directory') as m: m.side_effect = ((display_util.CANCEL, -1), (display_util.OK, self.path,)) self.auth.perform([self.achall]) assert self.config.webroot_map[self.achall.domain] == self.path @test_util.patch_display_util() def test_new_webroot_empty_map_cancel(self, mock_get_utility): self.config.webroot_path = [] self.config.webroot_map = {} mock_display = mock_get_utility() mock_display.menu.return_value = (display_util.OK, 0,) with mock.patch('certbot.display.ops.validated_directory') as m: m.return_value = (display_util.CANCEL, -1) with pytest.raises(errors.PluginError): self.auth.perform([self.achall]) def test_perform_missing_root(self): self.config.webroot_path = None self.config.webroot_map = {} with pytest.raises(errors.PluginError): self.auth.perform([]) def test_perform_reraises_other_errors(self): self.auth.full_path = os.path.join(self.path, "null") permission_canary = os.path.join(self.path, "rnd") with open(permission_canary, "w") as f: f.write("thingimy") filesystem.chmod(self.path, 0o000) try: with open(permission_canary, "r"): pass print("Warning, running tests as root skips permissions tests...") except IOError: # ok, permissions work, test away... with pytest.raises(errors.PluginError): self.auth.perform([]) filesystem.chmod(self.path, 0o700) @mock.patch("certbot._internal.plugins.webroot.filesystem.copy_ownership_and_apply_mode") def test_failed_chown(self, mock_ownership): mock_ownership.side_effect = OSError(errno.EACCES, "msg") self.auth.perform([self.achall]) # exception caught and logged @test_util.patch_display_util() def test_perform_new_webroot_not_in_map(self, mock_get_utility): new_webroot = tempfile.mkdtemp() self.config.webroot_path = [] self.config.webroot_map = {"whatever.com": self.path} mock_display = mock_get_utility() mock_display.menu.side_effect = ((display_util.OK, 0), (display_util.OK, new_webroot)) achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="something.com", account_key=KEY) with mock.patch('certbot.display.ops.validated_directory') as m: m.return_value = (display_util.OK, new_webroot,) self.auth.perform([achall]) assert self.config.webroot_map[achall.domain] == new_webroot def test_perform_permissions(self): self.auth.prepare() # Remove exec bit from permission check, so that it # matches the file self.auth.perform([self.achall]) assert filesystem.check_mode(self.validation_path, 0o644) # Check permissions of the directories for dirpath, dirnames, _ in os.walk(self.path): for directory in dirnames: full_path = os.path.join(dirpath, directory) assert filesystem.check_mode(full_path, 0o755) assert filesystem.has_same_ownership(self.validation_path, self.path) def test_perform_cleanup(self): self.auth.prepare() responses = self.auth.perform([self.achall]) assert 1 == len(responses) assert os.path.exists(self.validation_path) with open(self.validation_path) as validation_f: validation = validation_f.read() assert challenges.KeyAuthorizationChallengeResponse( key_authorization=validation).verify( self.achall.chall, KEY.public_key()) self.auth.cleanup([self.achall]) assert not os.path.exists(self.validation_path) assert not os.path.exists(self.root_challenge_path) assert not os.path.exists(self.partial_root_challenge_path) def test_perform_cleanup_existing_dirs(self): filesystem.mkdir(self.partial_root_challenge_path) self.auth.prepare() self.auth.perform([self.achall]) self.auth.cleanup([self.achall]) # Ensure we don't "clean up" directories that previously existed assert not os.path.exists(self.validation_path) assert not os.path.exists(self.root_challenge_path) def test_perform_cleanup_multiple_challenges(self): bingo_achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( challenges.HTTP01(token=b"bingo"), "pending"), domain="thing.com", account_key=KEY) bingo_validation_path = "YmluZ28" filesystem.mkdir(self.partial_root_challenge_path) self.auth.prepare() self.auth.perform([bingo_achall, self.achall]) self.auth.cleanup([self.achall]) assert not os.path.exists(bingo_validation_path) assert os.path.exists(self.root_challenge_path) self.auth.cleanup([bingo_achall]) assert not os.path.exists(self.validation_path) assert not os.path.exists(self.root_challenge_path) def test_cleanup_leftovers(self): self.auth.prepare() self.auth.perform([self.achall]) leftover_path = os.path.join(self.root_challenge_path, 'leftover') filesystem.mkdir(leftover_path) self.auth.cleanup([self.achall]) assert not os.path.exists(self.validation_path) assert os.path.exists(self.root_challenge_path) os.rmdir(leftover_path) @mock.patch('certbot.compat.os.rmdir') def test_cleanup_failure(self, mock_rmdir): self.auth.prepare() self.auth.perform([self.achall]) os_error = OSError() os_error.errno = errno.EACCES mock_rmdir.side_effect = os_error self.auth.cleanup([self.achall]) assert not os.path.exists(self.validation_path) assert os.path.exists(self.root_challenge_path) class WebrootActionTest(unittest.TestCase): """Tests for webroot argparse actions.""" achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="thing.com", account_key=KEY) def setUp(self): from certbot._internal.plugins.webroot import Authenticator self.path = tempfile.mkdtemp() self.parser = argparse.ArgumentParser() self.parser.add_argument("-d", "--domains", action="append", default=[]) Authenticator.inject_parser_options(self.parser, "webroot") def test_webroot_map_action(self): args = self.parser.parse_args( ["--webroot-map", json.dumps({'thing.com': self.path})]) assert args.webroot_map["thing.com"] == self.path def test_domain_before_webroot(self): args = self.parser.parse_args( "-d {0} -w {1}".format(self.achall.domain, self.path).split()) config = self._get_config_after_perform(args) assert config.webroot_map[self.achall.domain] == self.path def test_domain_before_webroot_error(self): with pytest.raises(errors.PluginError): self.parser.parse_args("-d foo -w bar -w baz".split()) with pytest.raises(errors.PluginError): self.parser.parse_args("-d foo -w bar -d baz -w qux".split()) def test_multiwebroot(self): args = self.parser.parse_args("-w {0} -d {1} -w {2} -d bar".format( self.path, self.achall.domain, tempfile.mkdtemp()).split()) assert args.webroot_map[self.achall.domain] == self.path config = self._get_config_after_perform(args) assert config.webroot_map[self.achall.domain] == self.path def test_webroot_map_partial_without_perform(self): # This test acknowledges the fact that webroot_map content will be partial if webroot # plugin perform method is not invoked (corner case when all auths are already valid). # To not be a problem, the webroot_path must always been conserved during renew. # This condition is challenged by: # certbot.tests.renewal_tests::RenewalTest::test_webroot_params_conservation # See https://github.com/certbot/certbot/pull/7095 for details. other_webroot_path = tempfile.mkdtemp() args = self.parser.parse_args("-w {0} -d {1} -w {2} -d bar".format( self.path, self.achall.domain, other_webroot_path).split()) assert args.webroot_map == {self.achall.domain: self.path} assert args.webroot_path == [self.path, other_webroot_path] def _get_config_after_perform(self, config): from certbot._internal.plugins.webroot import Authenticator auth = Authenticator(config, "webroot") auth.perform([self.achall]) return auth.config if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/renewal_test.py0000664000175100017510000003513014561227515022017 0ustar00ericaerica"""Tests for certbot._internal.renewal""" import copy import sys import unittest from unittest import mock import pytest from acme import challenges from certbot import configuration from certbot import errors from certbot._internal import storage import certbot.tests.util as test_util class RenewalTest(test_util.ConfigTestCase): @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_ancient_webroot_renewal_conf(self, mock_set_by_user): mock_set_by_user.return_value = False rc_path = test_util.make_lineage( self.config.config_dir, 'sample-renewal-ancient.conf') self.config.account = None self.config.email = None self.config.webroot_path = None config = configuration.NamespaceConfig(self.config) lineage = storage.RenewableCert(rc_path, config) renewalparams = lineage.configuration['renewalparams'] # pylint: disable=protected-access from certbot._internal import renewal renewal._restore_webroot_config(config, renewalparams) assert config.webroot_path == ['/var/www/'] @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_webroot_params_conservation(self, mock_set_by_user): # For more details about why this test is important, see: # certbot._internal.plugins.webroot_test:: # WebrootActionTest::test_webroot_map_partial_without_perform from certbot._internal import renewal mock_set_by_user.return_value = False renewalparams = { 'webroot_map': {'test.example.com': '/var/www/test'}, 'webroot_path': ['/var/www/test', '/var/www/other'], } renewal._restore_webroot_config(self.config, renewalparams) # pylint: disable=protected-access assert self.config.webroot_map == {'test.example.com': '/var/www/test'} assert self.config.webroot_path == ['/var/www/test', '/var/www/other'] renewalparams = { 'webroot_map': {}, 'webroot_path': '/var/www/test', } renewal._restore_webroot_config(self.config, renewalparams) # pylint: disable=protected-access assert self.config.webroot_map == {} assert self.config.webroot_path == ['/var/www/test'] @mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts') def test_reuse_key_renewal_params(self, unused_mock_avoid_reuse_conflicts): self.config.elliptic_curve = 'INVALID_VALUE' self.config.reuse_key = True self.config.dry_run = True config = configuration.NamespaceConfig(self.config) rc_path = test_util.make_lineage( self.config.config_dir, 'sample-renewal.conf') lineage = storage.RenewableCert(rc_path, config) le_client = mock.MagicMock() le_client.obtain_certificate.return_value = (None, None, None, None) from certbot._internal import renewal with mock.patch('certbot._internal.renewal.hooks.renew_hook'): renewal.renew_cert(self.config, None, le_client, lineage) assert self.config.elliptic_curve == 'secp256r1' @mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts') def test_reuse_ec_key_renewal_params(self, unused_mock_avoid_reuse_conflicts): self.config.elliptic_curve = 'INVALID_CURVE' self.config.reuse_key = True self.config.dry_run = True self.config.key_type = 'ecdsa' config = configuration.NamespaceConfig(self.config) rc_path = test_util.make_lineage( self.config.config_dir, 'sample-renewal-ec.conf', ec=True, ) lineage = storage.RenewableCert(rc_path, config) le_client = mock.MagicMock() le_client.obtain_certificate.return_value = (None, None, None, None) from certbot._internal import renewal with mock.patch('certbot._internal.renewal.hooks.renew_hook'): renewal.renew_cert(self.config, None, le_client, lineage) assert self.config.elliptic_curve == 'secp256r1' @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_new_key(self, mock_set_by_user): mock_set_by_user.return_value = False # When renewing with both reuse_key and new_key, the key should be regenerated, # the key type, key parameters and reuse_key should be kept. self.config.reuse_key = True self.config.new_key = True self.config.dry_run = True config = configuration.NamespaceConfig(self.config) rc_path = test_util.make_lineage( self.config.config_dir, 'sample-renewal.conf') lineage = storage.RenewableCert(rc_path, config) le_client = mock.MagicMock() le_client.obtain_certificate.return_value = (None, None, None, None) from certbot._internal import renewal with mock.patch('certbot._internal.renewal.hooks.renew_hook'): renewal.renew_cert(self.config, None, le_client, lineage) assert self.config.elliptic_curve == 'secp256r1' assert self.config.key_type == 'ecdsa' assert self.config.reuse_key # None is passed as the existing key, i.e. the key is not actually being reused. le_client.obtain_certificate.assert_called_with(mock.ANY, None) @mock.patch('certbot._internal.renewal.hooks.renew_hook') @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_reuse_key_conflicts(self, mock_set_by_user, unused_mock_renew_hook): mock_set_by_user.return_value = False # When renewing with reuse_key and a conflicting key parameter (size, curve) # an error should be raised ... self.config.reuse_key = True self.config.key_type = "rsa" self.config.rsa_key_size = 4096 self.config.dry_run = True config = configuration.NamespaceConfig(self.config) rc_path = test_util.make_lineage( self.config.config_dir, 'sample-renewal.conf') lineage = storage.RenewableCert(rc_path, config) lineage.configuration["renewalparams"]["reuse_key"] = True le_client = mock.MagicMock() le_client.obtain_certificate.return_value = (None, None, None, None) from certbot._internal import renewal with pytest.raises(errors.Error, match="Unable to change the --key-type"): renewal.renew_cert(self.config, None, le_client, lineage) # ... unless --no-reuse-key is set mock_set_by_user.side_effect = lambda var: var == "reuse_key" self.config.reuse_key = False renewal.renew_cert(self.config, None, le_client, lineage) @test_util.patch_display_util() @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_remove_deprecated_config_elements(self, mock_set_by_user, unused_mock_get_utility): mock_set_by_user.return_value = False config = configuration.NamespaceConfig(self.config) config.certname = "sample-renewal-deprecated-option" rc_path = test_util.make_lineage( self.config.config_dir, 'sample-renewal-deprecated-option.conf') from certbot._internal import renewal lineage_config = copy.deepcopy(self.config) renewal_candidate = renewal.reconstitute(lineage_config, rc_path) # This means that manual_public_ip_logging_ok was not modified in the config based on its # value in the renewal conf file assert isinstance(lineage_config.manual_public_ip_logging_ok, mock.MagicMock) @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_absent_key_type_restored(self, mock_set_by_user): mock_set_by_user.return_value = False rc_path = test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf', ec=False) from certbot._internal import renewal lineage_config = copy.deepcopy(self.config) renewal.reconstitute(lineage_config, rc_path) assert lineage_config.key_type == 'rsa' class RestoreRequiredConfigElementsTest(test_util.ConfigTestCase): """Tests for certbot._internal.renewal.restore_required_config_elements.""" @classmethod def _call(cls, *args, **kwargs): from certbot._internal.renewal import restore_required_config_elements return restore_required_config_elements(*args, **kwargs) @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_allow_subset_of_names_success(self, mock_set_by_user): mock_set_by_user.return_value = False self._call(self.config, {'allow_subset_of_names': 'True'}) assert self.config.allow_subset_of_names is True @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_allow_subset_of_names_failure(self, mock_set_by_user): mock_set_by_user.return_value = False renewalparams = {'allow_subset_of_names': 'maybe'} with pytest.raises(errors.Error): self._call(self.config, renewalparams) @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_pref_challs_list(self, mock_set_by_user): mock_set_by_user.return_value = False renewalparams = {'pref_challs': 'http-01, dns'.split(',')} self._call(self.config, renewalparams) expected = [challenges.HTTP01.typ, challenges.DNS01.typ] assert self.config.pref_challs == expected @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_pref_challs_str(self, mock_set_by_user): mock_set_by_user.return_value = False renewalparams = {'pref_challs': 'dns'} self._call(self.config, renewalparams) expected = [challenges.DNS01.typ] assert self.config.pref_challs == expected @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_pref_challs_failure(self, mock_set_by_user): mock_set_by_user.return_value = False renewalparams = {'pref_challs': 'finding-a-shrubbery'} with pytest.raises(errors.Error): self._call(self.config, renewalparams) @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_must_staple_success(self, mock_set_by_user): mock_set_by_user.return_value = False self._call(self.config, {'must_staple': 'True'}) assert self.config.must_staple is True @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_must_staple_failure(self, mock_set_by_user): mock_set_by_user.return_value = False renewalparams = {'must_staple': 'maybe'} with pytest.raises(errors.Error): self._call(self.config, renewalparams) @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') def test_ancient_server_renewal_conf(self, mock_set_by_user): from certbot._internal import constants self.config.server = None mock_set_by_user.return_value = False self._call(self.config, {'server': constants.V1_URI}) assert self.config.server == constants.CLI_DEFAULTS['server'] def test_related_values(self): # certbot.configuration.NamespaceConfig.set_by_user considers some values as related to each # other and considers both set by the user if either is. This test ensures all renewal # parameters are restored regardless of their restoration order or relation between values. # See https://github.com/certbot/certbot/issues/9805 for more info. renewalparams = { 'server': 'https://example.org', 'account': 'somehash', } self._call(self.config, renewalparams) self.assertEqual(self.config.account, renewalparams['account']) class DescribeResultsTest(unittest.TestCase): """Tests for certbot._internal.renewal._renew_describe_results.""" def setUp(self): self.patchers = { 'log_error': mock.patch('certbot._internal.renewal.logger.error'), 'notify': mock.patch('certbot._internal.renewal.display_util.notify')} self.mock_notify = self.patchers['notify'].start() self.mock_error = self.patchers['log_error'].start() def tearDown(self): for patch in self.patchers.values(): patch.stop() @classmethod def _call(cls, *args, **kwargs): from certbot._internal.renewal import _renew_describe_results _renew_describe_results(*args, **kwargs) def _assert_success_output(self, lines): self.mock_notify.assert_has_calls([mock.call(l) for l in lines]) def test_no_renewal_attempts(self): self._call(mock.MagicMock(dry_run=True), [], [], [], []) self._assert_success_output(['No simulated renewals were attempted.']) def test_successful_renewal(self): self._call(mock.MagicMock(dry_run=False), ['good.pem'], None, None, None) self._assert_success_output([ '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', 'Congratulations, all renewals succeeded: ', ' good.pem (success)', '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', ]) def test_failed_renewal(self): self._call(mock.MagicMock(dry_run=False), [], ['bad.pem'], [], []) self._assert_success_output([ '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', ]) self.mock_error.assert_has_calls([ mock.call('All %ss failed. The following certificates could not be renewed:', 'renewal'), mock.call(' bad.pem (failure)'), ]) def test_all_renewal(self): self._call(mock.MagicMock(dry_run=True), ['good.pem', 'good2.pem'], ['bad.pem', 'bad2.pem'], ['foo.pem expires on 123'], ['errored.conf']) self._assert_success_output([ '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', 'The following certificates are not due for renewal yet:', ' foo.pem expires on 123 (skipped)', 'The following simulated renewals succeeded:', ' good.pem (success)\n good2.pem (success)\n', '\nAdditionally, the following renewal configurations were invalid: ', ' errored.conf (parsefail)', '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', ]) self.mock_error.assert_has_calls([ mock.call('The following %ss failed:', 'simulated renewal'), mock.call(' bad.pem (failure)\n bad2.pem (failure)'), ]) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/renewupdater_test.py0000664000175100017510000001260514561227515023071 0ustar00ericaerica"""Tests for renewal updater interfaces""" import sys import unittest from unittest import mock import pytest from certbot import interfaces from certbot._internal import main from certbot._internal import updater from certbot.plugins import enhancements import certbot.tests.util as test_util class RenewUpdaterTest(test_util.ConfigTestCase): """Tests for interfaces.RenewDeployer and interfaces.GenericUpdater""" def setUp(self): super().setUp() self.generic_updater = mock.MagicMock(spec=interfaces.GenericUpdater) self.generic_updater.restart = mock.MagicMock() self.renew_deployer = mock.MagicMock(spec=interfaces.RenewDeployer) self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement) @mock.patch('certbot._internal.main._get_and_save_cert') @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins') @mock.patch('certbot._internal.plugins.selection.get_unprepared_installer') @test_util.patch_display_util() def test_server_updates(self, _, mock_geti, mock_select, mock_getsave): mock_getsave.return_value = mock.MagicMock() mock_generic_updater = self.generic_updater # Generic Updater mock_select.return_value = (mock_generic_updater, None) mock_geti.return_value = mock_generic_updater with mock.patch('certbot._internal.main._init_le_client'): main.renew_cert(self.config, None, mock.MagicMock()) assert mock_generic_updater.restart.called mock_generic_updater.restart.reset_mock() mock_generic_updater.generic_updates.reset_mock() updater.run_generic_updaters(self.config, mock.MagicMock(), None) assert mock_generic_updater.generic_updates.call_count == 1 assert mock_generic_updater.restart.called is False def test_renew_deployer(self): lineage = mock.MagicMock() mock_deployer = self.renew_deployer updater.run_renewal_deployer(self.config, lineage, mock_deployer) mock_deployer.renew_deploy.assert_called_with(lineage) @mock.patch("certbot._internal.updater.logger.debug") def test_updater_skip_dry_run(self, mock_log): self.config.dry_run = True updater.run_generic_updaters(self.config, None, None) assert mock_log.called assert mock_log.call_args[0][0] == \ "Skipping updaters in dry-run mode." @mock.patch("certbot._internal.updater.logger.debug") def test_deployer_skip_dry_run(self, mock_log): self.config.dry_run = True updater.run_renewal_deployer(self.config, None, None) assert mock_log.called assert mock_log.call_args[0][0] == \ "Skipping renewal deployer in dry-run mode." @mock.patch('certbot._internal.plugins.selection.get_unprepared_installer') def test_enhancement_updates(self, mock_geti): mock_geti.return_value = self.mockinstaller updater.run_generic_updaters(self.config, mock.MagicMock(), None) assert self.mockinstaller.update_autohsts.called assert self.mockinstaller.update_autohsts.call_count == 1 def test_enhancement_deployer(self): updater.run_renewal_deployer(self.config, mock.MagicMock(), self.mockinstaller) assert self.mockinstaller.deploy_autohsts.called @mock.patch('certbot._internal.plugins.selection.get_unprepared_installer') def test_enhancement_updates_not_called(self, mock_geti): self.config.disable_renew_updates = True mock_geti.return_value = self.mockinstaller updater.run_generic_updaters(self.config, mock.MagicMock(), None) assert self.mockinstaller.update_autohsts.called is False def test_enhancement_deployer_not_called(self): self.config.disable_renew_updates = True updater.run_renewal_deployer(self.config, mock.MagicMock(), self.mockinstaller) assert self.mockinstaller.deploy_autohsts.called is False @mock.patch('certbot._internal.plugins.selection.get_unprepared_installer') def test_enhancement_no_updater(self, mock_geti): FAKEINDEX = [ { "name": "Test", "class": enhancements.AutoHSTSEnhancement, "updater_function": None, "deployer_function": "deploy_autohsts", "enable_function": "enable_autohsts" } ] mock_geti.return_value = self.mockinstaller with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX): updater.run_generic_updaters(self.config, mock.MagicMock(), None) assert self.mockinstaller.update_autohsts.called is False def test_enhancement_no_deployer(self): FAKEINDEX = [ { "name": "Test", "class": enhancements.AutoHSTSEnhancement, "updater_function": "deploy_autohsts", "deployer_function": None, "enable_function": "enable_autohsts" } ] with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX): updater.run_renewal_deployer(self.config, mock.MagicMock(), self.mockinstaller) assert self.mockinstaller.deploy_autohsts.called is False if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/reverter_test.py0000664000175100017510000004115114561227515022220 0ustar00ericaerica"""Test certbot.reverter.""" import csv import logging import shutil import sys import tempfile import unittest from unittest import mock import pytest from certbot import errors from certbot.compat import os from certbot.tests import util as test_util class ReverterCheckpointLocalTest(test_util.ConfigTestCase): """Test the Reverter Class.""" def setUp(self): super().setUp() from certbot.reverter import Reverter # Disable spurious errors... we are trying to test for them logging.disable(logging.CRITICAL) self.reverter = Reverter(self.config) tup = setup_test_files() self.config1, self.config2, self.dir1, self.dir2, self.sets = tup def tearDown(self): shutil.rmtree(self.config.work_dir) shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) logging.disable(logging.NOTSET) @mock.patch("certbot.reverter.Reverter._read_and_append") def test_no_change(self, mock_read): mock_read.side_effect = OSError("cannot even") try: self.reverter.add_to_checkpoint(self.sets[0], "save1") except OSError: pass self.reverter.finalize_checkpoint("blah") path = os.listdir(self.reverter.config.backup_dir)[0] no_change = os.path.join(self.reverter.config.backup_dir, path, "CHANGES_SINCE") with open(no_change, "r") as f: x = f.read() assert "No changes" in x def test_basic_add_to_temp_checkpoint(self): # These shouldn't conflict even though they are both named config.txt self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") self.reverter.add_to_temp_checkpoint(self.sets[1], "save2") assert os.path.isdir(self.config.temp_checkpoint_dir) assert get_save_notes( self.config.temp_checkpoint_dir) == "save1save2" assert not os.path.isfile( os.path.join(self.config.temp_checkpoint_dir, "NEW_FILES")) assert get_filepaths(self.config.temp_checkpoint_dir) == \ "{0}\n{1}\n".format(self.config1, self.config2) def test_add_to_checkpoint_copy_failure(self): with mock.patch("certbot.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = IOError("bad copy") with pytest.raises(errors.ReverterError): self.reverter.add_to_checkpoint(self.sets[0], "save1") def test_checkpoint_conflict(self): """Make sure that checkpoint errors are thrown appropriately.""" config3 = os.path.join(self.dir1, "config3.txt") self.reverter.register_file_creation(True, config3) update_file(config3, "This is a new file!") self.reverter.add_to_checkpoint(self.sets[2], "save1") # This shouldn't throw an error self.reverter.add_to_temp_checkpoint(self.sets[0], "save2") # Raise error with pytest.raises(errors.ReverterError): self.reverter.add_to_checkpoint(self.sets[2], "save3") # Should not cause an error self.reverter.add_to_checkpoint(self.sets[1], "save4") # Check to make sure new files are also checked... with pytest.raises(errors.ReverterError): self.reverter.add_to_checkpoint({config3}, "invalid save") def test_multiple_saves_and_temp_revert(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") update_file(self.config1, "updated-directive") self.reverter.add_to_temp_checkpoint(self.sets[0], "save2-updated dir") update_file(self.config1, "new directive change that we won't keep") self.reverter.revert_temporary_config() assert read_in(self.config1) == "directive-dir1" def test_multiple_registration_fail_and_revert(self): config3 = os.path.join(self.dir1, "config3.txt") update_file(config3, "Config3") config4 = os.path.join(self.dir2, "config4.txt") update_file(config4, "Config4") # Test multiple registrations and two registrations at once self.reverter.register_file_creation(True, self.config1) self.reverter.register_file_creation(True, self.config2) self.reverter.register_file_creation(True, config3, config4) # Simulate Certbot crash... recovery routine is run self.reverter.recovery_routine() assert not os.path.isfile(self.config1) assert not os.path.isfile(self.config2) assert not os.path.isfile(config3) assert not os.path.isfile(config4) def test_multiple_registration_same_file(self): self.reverter.register_file_creation(True, self.config1) self.reverter.register_file_creation(True, self.config1) self.reverter.register_file_creation(True, self.config1) self.reverter.register_file_creation(True, self.config1) files = get_new_files(self.config.temp_checkpoint_dir) assert len(files) == 1 def test_register_file_creation_write_error(self): m_open = mock.mock_open() with mock.patch("certbot.reverter.open", m_open, create=True): m_open.side_effect = OSError("bad open") with pytest.raises(errors.ReverterError): self.reverter.register_file_creation(True, self.config1) def test_bad_registration(self): # Made this mistake and want to make sure it doesn't happen again... with pytest.raises(errors.ReverterError): self.reverter.register_file_creation("filepath") def test_register_undo_command(self): coms = [ ["a2dismod", "ssl"], ["a2dismod", "rewrite"], ["cleanslate"] ] for com in coms: self.reverter.register_undo_command(True, com) act_coms = get_undo_commands(self.config.temp_checkpoint_dir) for a_com, com in zip(act_coms, coms): assert a_com == com def test_bad_register_undo_command(self): m_open = mock.mock_open() with mock.patch("certbot.reverter.open", m_open, create=True): m_open.side_effect = OSError("bad open") with pytest.raises(errors.ReverterError): self.reverter.register_undo_command(True, ["command"]) @mock.patch("certbot.util.run_script") def test_run_undo_commands(self, mock_run): mock_run.side_effect = ["", errors.SubprocessError] coms = [ ["invalid_command"], ["a2dismod", "ssl"], ] for com in coms: self.reverter.register_undo_command(True, com) self.reverter.revert_temporary_config() assert mock_run.call_count == 2 def test_recovery_routine_in_progress_failure(self): self.reverter.add_to_checkpoint(self.sets[0], "perm save") # pylint: disable=protected-access self.reverter._recover_checkpoint = mock.MagicMock( side_effect=errors.ReverterError) with pytest.raises(errors.ReverterError): self.reverter.recovery_routine() def test_recover_checkpoint_revert_temp_failures(self): mock_recover = mock.MagicMock( side_effect=errors.ReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_temp_checkpoint(self.sets[0], "config1 save") with pytest.raises(errors.ReverterError): self.reverter.revert_temporary_config() def test_recover_checkpoint_rollback_failure(self): mock_recover = mock.MagicMock( side_effect=errors.ReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_checkpoint(self.sets[0], "config1 save") self.reverter.finalize_checkpoint("Title") with pytest.raises(errors.ReverterError): self.reverter.rollback_checkpoints(1) def test_recover_checkpoint_copy_failure(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") with mock.patch("certbot.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = OSError("bad copy") with pytest.raises(errors.ReverterError): self.reverter.revert_temporary_config() def test_recover_checkpoint_rm_failure(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "temp save") with mock.patch("certbot.reverter.shutil.rmtree") as mock_rmtree: mock_rmtree.side_effect = OSError("Cannot remove tree") with pytest.raises(errors.ReverterError): self.reverter.revert_temporary_config() @mock.patch("certbot.reverter.logger.warning") def test_recover_checkpoint_missing_new_files(self, mock_warn): self.reverter.register_file_creation( True, os.path.join(self.dir1, "missing_file.txt")) self.reverter.revert_temporary_config() assert mock_warn.call_count == 1 @mock.patch("certbot.reverter.os.remove") def test_recover_checkpoint_remove_failure(self, mock_remove): self.reverter.register_file_creation(True, self.config1) mock_remove.side_effect = OSError("Can't remove") with pytest.raises(errors.ReverterError): self.reverter.revert_temporary_config() def test_recovery_routine_temp_and_perm(self): # Register a new perm checkpoint file config3 = os.path.join(self.dir1, "config3.txt") self.reverter.register_file_creation(False, config3) update_file(config3, "This is a new perm file!") # Add changes to perm checkpoint self.reverter.add_to_checkpoint(self.sets[0], "perm save1") update_file(self.config1, "updated perm config1") self.reverter.add_to_checkpoint(self.sets[1], "perm save2") update_file(self.config2, "updated perm config2") # Add changes to a temporary checkpoint self.reverter.add_to_temp_checkpoint(self.sets[0], "temp save1") update_file(self.config1, "second update now temp config1") # Register a new temp checkpoint file config4 = os.path.join(self.dir2, "config4.txt") self.reverter.register_file_creation(True, config4) update_file(config4, "New temporary file!") # Now erase everything self.reverter.recovery_routine() # Now Run tests # These were new files.. they should be removed assert not os.path.isfile(config3) assert not os.path.isfile(config4) # Check to make sure everything got rolled back appropriately assert read_in(self.config1) == "directive-dir1" assert read_in(self.config2) == "directive-dir2" class TestFullCheckpointsReverter(test_util.ConfigTestCase): """Tests functions having to deal with full checkpoints.""" def setUp(self): super().setUp() from certbot.reverter import Reverter # Disable spurious errors... logging.disable(logging.CRITICAL) self.reverter = Reverter(self.config) tup = setup_test_files() self.config1, self.config2, self.dir1, self.dir2, self.sets = tup def tearDown(self): shutil.rmtree(self.config.work_dir) shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) logging.disable(logging.NOTSET) def test_rollback_improper_inputs(self): with pytest.raises(errors.ReverterError): self.reverter.rollback_checkpoints("-1") with pytest.raises(errors.ReverterError): self.reverter.rollback_checkpoints(-1000) with pytest.raises(errors.ReverterError): self.reverter.rollback_checkpoints("one") def test_rollback_finalize_checkpoint_valid_inputs(self): config3 = self._setup_three_checkpoints() # Check resulting backup directory assert len(os.listdir(self.config.backup_dir)) == 3 # Check rollbacks # First rollback self.reverter.rollback_checkpoints(1) assert read_in(self.config1) == "update config1" assert read_in(self.config2) == "update config2" # config3 was not included in checkpoint assert read_in(config3) == "Final form config3" # Second rollback self.reverter.rollback_checkpoints(1) assert read_in(self.config1) == "update config1" assert read_in(self.config2) == "directive-dir2" assert not os.path.isfile(config3) # One dir left... check title all_dirs = os.listdir(self.config.backup_dir) assert len(all_dirs) == 1 assert "First Checkpoint" in get_save_notes( os.path.join(self.config.backup_dir, all_dirs[0])) # Final rollback self.reverter.rollback_checkpoints(1) assert read_in(self.config1) == "directive-dir1" def test_finalize_checkpoint_no_in_progress(self): # No need to warn for this... just make sure there are no errors. self.reverter.finalize_checkpoint("No checkpoint...") @mock.patch("certbot.reverter.shutil.move") def test_finalize_checkpoint_cannot_title(self, mock_move): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_move.side_effect = OSError("cannot move") with pytest.raises(errors.ReverterError): self.reverter.finalize_checkpoint("Title") @mock.patch("certbot.reverter.filesystem.replace") def test_finalize_checkpoint_no_rename_directory(self, mock_replace): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_replace.side_effect = OSError with pytest.raises(errors.ReverterError): self.reverter.finalize_checkpoint("Title") @mock.patch("certbot.reverter.logger") def test_rollback_too_many(self, mock_logger): # Test no exist warning... self.reverter.rollback_checkpoints(1) assert mock_logger.warning.call_count == 1 # Test Generic warning self._setup_three_checkpoints() mock_logger.warning.call_count = 0 self.reverter.rollback_checkpoints(4) assert mock_logger.warning.call_count == 1 def test_multi_rollback(self): config3 = self._setup_three_checkpoints() self.reverter.rollback_checkpoints(3) assert read_in(self.config1) == "directive-dir1" assert read_in(self.config2) == "directive-dir2" assert not os.path.isfile(config3) def _setup_three_checkpoints(self): """Generate some finalized checkpoints.""" # Checkpoint1 - config1 self.reverter.add_to_checkpoint(self.sets[0], "first save") self.reverter.finalize_checkpoint("First Checkpoint") update_file(self.config1, "update config1") # Checkpoint2 - new file config3, update config2 config3 = os.path.join(self.dir1, "config3.txt") self.reverter.register_file_creation(False, config3) update_file(config3, "directive-config3") self.reverter.add_to_checkpoint(self.sets[1], "second save") self.reverter.finalize_checkpoint("Second Checkpoint") update_file(self.config2, "update config2") update_file(config3, "update config3") # Checkpoint3 - update config1, config2 self.reverter.add_to_checkpoint(self.sets[2], "third save") self.reverter.finalize_checkpoint("Third Checkpoint - Save both") update_file(self.config1, "Final form config1") update_file(self.config2, "Final form config2") update_file(config3, "Final form config3") return config3 def setup_test_files(): """Setup sample configuration files.""" dir1 = tempfile.mkdtemp("dir1") dir2 = tempfile.mkdtemp("dir2") config1 = os.path.join(dir1, "config.txt") config2 = os.path.join(dir2, "config.txt") with open(config1, "w") as file_fd: file_fd.write("directive-dir1") with open(config2, "w") as file_fd: file_fd.write("directive-dir2") sets = [{config1}, {config2}, {config1, config2}] return config1, config2, dir1, dir2, sets def get_save_notes(dire): """Read save notes""" return read_in(os.path.join(dire, "CHANGES_SINCE")) def get_filepaths(dire): """Get Filepaths""" return read_in(os.path.join(dire, "FILEPATHS")) def get_new_files(dire): """Get new files.""" return read_in(os.path.join(dire, "NEW_FILES")).splitlines() def get_undo_commands(dire): """Get new files.""" with open(os.path.join(dire, "COMMANDS")) as csvfile: return list(csv.reader(csvfile)) def read_in(path): """Read in a file, return the str""" with open(path, "r") as file_fd: return file_fd.read() def update_file(filename, string): """Update a file with a new value.""" with open(filename, "w") as file_fd: file_fd.write(string) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/storage_test.py0000664000175100017510000013012014561227515022021 0ustar00ericaerica"""Tests for certbot._internal.storage.""" # pylint disable=protected-access import datetime import shutil import stat import sys import unittest from unittest import mock import configobj import pytest import pytz import certbot from certbot import configuration from certbot import errors from certbot._internal.storage import ALL_FOUR from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util CERT = test_util.load_cert('cert_512.pem') def unlink_all(rc_object): """Unlink all four items associated with this RenewableCert.""" for kind in ALL_FOUR: os.unlink(getattr(rc_object, kind)) def fill_with_sample_data(rc_object): """Put dummy data into all four files of this RenewableCert.""" for kind in ALL_FOUR: with open(getattr(rc_object, kind), "w") as f: f.write(kind) class RelevantValuesTest(unittest.TestCase): """Tests for certbot._internal.storage.relevant_values.""" def setUp(self): self.values = {"server": "example.org", "key_type": "rsa"} self.mock_config = mock.MagicMock() self.mock_config.set_by_user = mock.MagicMock() def _call(self, values): from certbot._internal.storage import relevant_values self.mock_config.to_dict.return_value = values return relevant_values(self.mock_config) @mock.patch("certbot._internal.plugins.disco.PluginsRegistry.find_all") def test_namespace(self, mock_find_all): mock_find_all.return_value = ["certbot-foo:bar"] self.mock_config.set_by_user.return_value = True self.values["certbot_foo:bar_baz"] = 42 assert self._call(self.values.copy()) == self.values def test_option_set(self): self.mock_config.set_by_user.return_value = True self.values["allow_subset_of_names"] = True self.values["authenticator"] = "apache" self.values["rsa_key_size"] = 1337 expected_relevant_values = self.values.copy() self.values["hello"] = "there" assert self._call(self.values) == expected_relevant_values def test_option_unset(self): self.mock_config.set_by_user.return_value = False expected_relevant_values = self.values.copy() self.values["rsa_key_size"] = 2048 assert self._call(self.values) == expected_relevant_values def test_deprecated_item(self): deprected_option = 'manual_public_ip_logging_ok' self.mock_config.set_by_user = lambda v: False if v == deprected_option else True # deprecated items should never be relevant to store expected_relevant_values = self.values.copy() self.values[deprected_option] = None assert self._call(self.values) == expected_relevant_values self.values[deprected_option] = True assert self._call(self.values) == expected_relevant_values self.values[deprected_option] = False assert self._call(self.values) == expected_relevant_values def test_with_real_parser(self): from certbot._internal.storage import relevant_values from certbot._internal.plugins import disco from certbot._internal import cli from certbot._internal import constants PLUGINS = disco.PluginsRegistry.find_all() namespace = cli.prepare_and_parse_args(PLUGINS, [ '--allow-subset-of-names', '--authenticator', 'apache', ]) expected_relevant_values = { 'server': constants.CLI_DEFAULTS['server'], 'key_type': 'ecdsa', 'allow_subset_of_names': True, 'authenticator': 'apache', } assert relevant_values(namespace) == expected_relevant_values class BaseRenewableCertTest(test_util.ConfigTestCase): """Base class for setting up Renewable Cert tests. .. note:: It may be required to write out self.config for your test. Check :class:`.cli_test.DuplicateCertTest` for an example. """ def setUp(self): from certbot._internal import storage super().setUp() # TODO: maybe provide NamespaceConfig.make_dirs? # TODO: main() should create those dirs, c.f. #902 filesystem.makedirs(os.path.join(self.config.config_dir, "live", "example.org")) archive_path = os.path.join(self.config.config_dir, "archive", "example.org") filesystem.makedirs(archive_path) filesystem.makedirs(os.path.join(self.config.config_dir, "renewal")) config_file = configobj.ConfigObj() for kind in ALL_FOUR: kind_path = os.path.join(self.config.config_dir, "live", "example.org", kind + ".pem") config_file[kind] = kind_path with open(os.path.join(self.config.config_dir, "live", "example.org", "README"), 'a'): pass config_file["archive"] = archive_path config_file.filename = os.path.join(self.config.config_dir, "renewal", "example.org.conf") config_file.write() self.config_file = config_file # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. with open(os.path.join(self.config.config_dir, "renewal", "IGNORE.THIS"), "w") as junk: junk.write("This file should be ignored!") self.defaults = configobj.ConfigObj() with mock.patch("certbot._internal.storage.RenewableCert._check_symlinks") as check: check.return_value = True self.test_rc = storage.RenewableCert(config_file.filename, self.config) def _write_out_kind(self, kind, ver, value=None): link = getattr(self.test_rc, kind) if os.path.lexists(link): os.unlink(link) os.symlink(os.path.join(os.path.pardir, os.path.pardir, "archive", "example.org", "{0}{1}.pem".format(kind, ver)), link) with open(link, "wb") as f: f.write(kind.encode('ascii') if value is None else value) if kind == "privkey": filesystem.chmod(link, 0o600) def _write_out_ex_kinds(self): for kind in ALL_FOUR: self._write_out_kind(kind, 12) self._write_out_kind(kind, 11) class RenewableCertTests(BaseRenewableCertTest): """Tests for certbot._internal.storage.""" def test_initialization(self): assert self.test_rc.lineagename == "example.org" for kind in ALL_FOUR: assert getattr(self.test_rc, kind) == os.path.join( self.config.config_dir, "live", "example.org", kind + ".pem") def test_renewal_bad_config(self): """Test that the RenewableCert constructor will complain if the renewal configuration file doesn't end in ".conf" """ from certbot._internal import storage broken = os.path.join(self.config.config_dir, "broken.conf") with open(broken, "w") as f: f.write("[No closing bracket for you!") with pytest.raises(errors.CertStorageError): storage.RenewableCert(broken, self.config) os.unlink(broken) with pytest.raises(errors.CertStorageError): storage.RenewableCert("fun", self.config) def test_renewal_incomplete_config(self): """Test that the RenewableCert constructor will complain if the renewal configuration file is missing a required file element.""" from certbot._internal import storage config = configobj.ConfigObj() config["cert"] = "imaginary_cert.pem" # Here the required privkey is missing. config["chain"] = "imaginary_chain.pem" config["fullchain"] = "imaginary_fullchain.pem" config.filename = os.path.join(self.config.config_dir, "imaginary_config.conf") config.write() with pytest.raises(errors.CertStorageError): storage.RenewableCert(config.filename, self.config) def test_no_renewal_version(self): from certbot._internal import storage self._write_out_ex_kinds() assert "version" not in self.config_file with mock.patch("certbot._internal.storage.logger") as mock_logger: storage.RenewableCert(self.config_file.filename, self.config) assert mock_logger.warning.called is False def test_renewal_newer_version(self): from certbot._internal import storage self._write_out_ex_kinds() self.config_file["version"] = "99.99.99" self.config_file.write() with mock.patch("certbot._internal.storage.logger") as mock_logger: storage.RenewableCert(self.config_file.filename, self.config) assert mock_logger.info.called assert "version" in mock_logger.info.call_args[0][0] def test_consistent(self): # pylint: disable=protected-access oldcert = self.test_rc.cert self.test_rc.cert = "relative/path" # Absolute path for item requirement assert not self.test_rc._consistent() self.test_rc.cert = oldcert # Items must exist requirement assert not self.test_rc._consistent() # Items must be symlinks requirements fill_with_sample_data(self.test_rc) assert not self.test_rc._consistent() unlink_all(self.test_rc) # Items must point to desired place if they are relative for kind in ALL_FOUR: os.symlink(os.path.join("..", kind + "17.pem"), getattr(self.test_rc, kind)) assert not self.test_rc._consistent() unlink_all(self.test_rc) # Items must point to desired place if they are absolute for kind in ALL_FOUR: os.symlink(os.path.join(self.config.config_dir, kind + "17.pem"), getattr(self.test_rc, kind)) assert not self.test_rc._consistent() unlink_all(self.test_rc) # Items must point to things that exist for kind in ALL_FOUR: os.symlink(os.path.join("..", "..", "archive", "example.org", kind + "17.pem"), getattr(self.test_rc, kind)) assert not self.test_rc._consistent() # This version should work fill_with_sample_data(self.test_rc) assert self.test_rc._consistent() # Items must point to things that follow the naming convention os.unlink(self.test_rc.fullchain) os.symlink(os.path.join("..", "..", "archive", "example.org", "fullchain_17.pem"), self.test_rc.fullchain) with open(self.test_rc.fullchain, "w") as f: f.write("wrongly-named fullchain") assert not self.test_rc._consistent() def test_current_target(self): # Relative path logic self._write_out_kind("cert", 17) assert os.path.samefile(self.test_rc.current_target("cert"), os.path.join(self.config.config_dir, "archive", "example.org", "cert17.pem")) # Absolute path logic os.unlink(self.test_rc.cert) os.symlink(os.path.join(self.config.config_dir, "archive", "example.org", "cert17.pem"), self.test_rc.cert) with open(self.test_rc.cert, "w") as f: f.write("cert") assert os.path.samefile(self.test_rc.current_target("cert"), os.path.join(self.config.config_dir, "archive", "example.org", "cert17.pem")) def test_current_version(self): for ver in (1, 5, 10, 20): self._write_out_kind("cert", ver) os.unlink(self.test_rc.cert) os.symlink(os.path.join("..", "..", "archive", "example.org", "cert10.pem"), self.test_rc.cert) assert self.test_rc.current_version("cert") == 10 def test_no_current_version(self): assert self.test_rc.current_version("cert") is None def test_latest_and_next_versions(self): for ver in range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) assert self.test_rc.latest_common_version() == 5 assert self.test_rc.next_free_version() == 6 # Having one kind of file of a later version doesn't change the # result self._write_out_kind("privkey", 7) assert self.test_rc.latest_common_version() == 5 # ... although it does change the next free version assert self.test_rc.next_free_version() == 8 # Nor does having three out of four change the result self._write_out_kind("cert", 7) self._write_out_kind("fullchain", 7) assert self.test_rc.latest_common_version() == 5 # If we have everything from a much later version, it does change # the result for kind in ALL_FOUR: self._write_out_kind(kind, 17) assert self.test_rc.latest_common_version() == 17 assert self.test_rc.next_free_version() == 18 @mock.patch("certbot._internal.storage.logger") def test_ensure_deployed(self, mock_logger): mock_update = self.test_rc.update_all_links_to = mock.Mock() mock_has_pending = self.test_rc.has_pending_deployment = mock.Mock() self.test_rc.latest_common_version = mock.Mock() mock_has_pending.return_value = False assert self.test_rc.ensure_deployed() is True assert mock_update.call_count == 0 assert mock_logger.warning.call_count == 0 mock_has_pending.return_value = True assert self.test_rc.ensure_deployed() is False assert mock_update.call_count == 1 assert mock_logger.warning.call_count == 1 def test_update_link_to(self): for ver in range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) assert ver == self.test_rc.current_version(kind) # pylint: disable=protected-access self.test_rc._update_link_to("cert", 3) self.test_rc._update_link_to("privkey", 2) assert 3 == self.test_rc.current_version("cert") assert 2 == self.test_rc.current_version("privkey") assert 5 == self.test_rc.current_version("chain") assert 5 == self.test_rc.current_version("fullchain") # Currently we are allowed to update to a version that doesn't exist self.test_rc._update_link_to("chain", 3000) # However, current_version doesn't allow querying the resulting # version (because it's a broken link). assert os.path.basename(filesystem.readlink(self.test_rc.chain)) == \ "chain3000.pem" def test_version(self): self._write_out_kind("cert", 12) # TODO: We should probably test that the directory is still the # same, but it's tricky because we can get an absolute # path out when we put a relative path in. assert "cert8.pem" == \ os.path.basename(self.test_rc.version("cert", 8)) def test_update_all_links_to_success(self): for ver in range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) assert ver == self.test_rc.current_version(kind) assert self.test_rc.latest_common_version() == 5 for ver in range(1, 6): self.test_rc.update_all_links_to(ver) for kind in ALL_FOUR: assert ver == self.test_rc.current_version(kind) assert self.test_rc.latest_common_version() == 5 def test_update_all_links_to_partial_failure(self): def unlink_or_raise(path, real_unlink=os.unlink): # pylint: disable=missing-docstring basename = os.path.basename(path) if "fullchain" in basename and basename.startswith("prev"): raise ValueError real_unlink(path) self._write_out_ex_kinds() with mock.patch("certbot._internal.storage.os.unlink") as mock_unlink: mock_unlink.side_effect = unlink_or_raise with pytest.raises(ValueError): self.test_rc.update_all_links_to(12) for kind in ALL_FOUR: assert self.test_rc.current_version(kind) == 12 def test_update_all_links_to_full_failure(self): def unlink_or_raise(path, real_unlink=os.unlink): # pylint: disable=missing-docstring if "fullchain" in os.path.basename(path): raise ValueError real_unlink(path) self._write_out_ex_kinds() with mock.patch("certbot._internal.storage.os.unlink") as mock_unlink: mock_unlink.side_effect = unlink_or_raise with pytest.raises(ValueError): self.test_rc.update_all_links_to(12) for kind in ALL_FOUR: assert self.test_rc.current_version(kind) == 11 def test_has_pending_deployment(self): for ver in range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) assert ver == self.test_rc.current_version(kind) for ver in range(1, 6): self.test_rc.update_all_links_to(ver) for kind in ALL_FOUR: assert ver == self.test_rc.current_version(kind) if ver < 5: assert self.test_rc.has_pending_deployment() else: assert not self.test_rc.has_pending_deployment() def test_names(self): # Trying the current version self._write_out_kind("cert", 12, test_util.load_vector("cert-san_512.pem")) assert self.test_rc.names() == \ ["example.com", "www.example.com"] # Trying missing cert os.unlink(self.test_rc.cert) with pytest.raises(errors.CertStorageError): self.test_rc.names() @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') @mock.patch("certbot._internal.storage.datetime") def test_time_interval_judgments(self, mock_datetime, mock_set_by_user): """Test should_autorenew() on the basis of expiry time windows.""" test_cert = test_util.load_vector("cert_512.pem") self._write_out_ex_kinds() self.test_rc.update_all_links_to(12) with open(self.test_rc.cert, "wb") as f: f.write(test_cert) self.test_rc.update_all_links_to(11) with open(self.test_rc.cert, "wb") as f: f.write(test_cert) mock_datetime.timedelta = datetime.timedelta mock_set_by_user.return_value = False self.test_rc.configuration["renewalparams"] = {} for (current_time, interval, result) in [ # 2014-12-13 12:00:00+00:00 (about 5 days prior to expiry) # Times that should result in autorenewal/autodeployment (1418472000, "2 months", True), (1418472000, "1 week", True), # Times that should not (1418472000, "4 days", False), (1418472000, "2 days", False), # 2009-05-01 12:00:00+00:00 (about 5 years prior to expiry) # Times that should result in autorenewal/autodeployment (1241179200, "7 years", True), (1241179200, "11 years 2 months", True), # Times that should not (1241179200, "8 hours", False), (1241179200, "2 days", False), (1241179200, "40 days", False), (1241179200, "9 months", False), # 2015-01-01 (after expiry has already happened, so all # intervals should cause autorenewal/autodeployment) (1420070400, "0 seconds", True), (1420070400, "10 seconds", True), (1420070400, "10 minutes", True), (1420070400, "10 weeks", True), (1420070400, "10 months", True), (1420070400, "10 years", True), (1420070400, "99 months", True), ]: sometime = datetime.datetime.fromtimestamp(current_time, pytz.UTC) mock_datetime.datetime.now.return_value = sometime self.test_rc.configuration["renew_before_expiry"] = interval assert self.test_rc.should_autorenew() == result def test_autorenewal_is_enabled(self): self.test_rc.configuration["renewalparams"] = {} assert self.test_rc.autorenewal_is_enabled() self.test_rc.configuration["renewalparams"]["autorenew"] = "True" assert self.test_rc.autorenewal_is_enabled() self.test_rc.configuration["renewalparams"]["autorenew"] = "False" assert not self.test_rc.autorenewal_is_enabled() @mock.patch.object(configuration.NamespaceConfig, 'set_by_user') @mock.patch("certbot._internal.storage.RenewableCert.ocsp_revoked") def test_should_autorenew(self, mock_ocsp, mock_set_by_user): """Test should_autorenew on the basis of reasons other than expiry time window.""" mock_set_by_user.return_value = False # Autorenewal turned off self.test_rc.configuration["renewalparams"] = {"autorenew": "False"} assert not self.test_rc.should_autorenew() self.test_rc.configuration["renewalparams"]["autorenew"] = "True" for kind in ALL_FOUR: self._write_out_kind(kind, 12) # Mandatory renewal on the basis of OCSP revocation mock_ocsp.return_value = True assert self.test_rc.should_autorenew() mock_ocsp.return_value = False @mock.patch("certbot._internal.storage.relevant_values") def test_save_successor(self, mock_rv): # Mock relevant_values() to claim that all values are relevant here # (to avoid instantiating parser) mock_rv.side_effect = lambda x: x.to_dict() for ver in range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) self.test_rc.update_all_links_to(3) assert 6 == self.test_rc.save_successor(3, b'new cert', None, b'new chain', self.config) with open(self.test_rc.version("cert", 6)) as f: assert f.read() == "new cert" with open(self.test_rc.version("chain", 6)) as f: assert f.read() == "new chain" with open(self.test_rc.version("fullchain", 6)) as f: assert f.read() == "new cert" + "new chain" # version 6 of the key should be a link back to version 3 assert not os.path.islink(self.test_rc.version("privkey", 3)) assert os.path.islink(self.test_rc.version("privkey", 6)) # Let's try two more updates assert 7 == self.test_rc.save_successor(6, b'again', None, b'newer chain', self.config) assert 8 == self.test_rc.save_successor(7, b'hello', None, b'other chain', self.config) # All of the subsequent versions should link directly to the original # privkey. for i in (6, 7, 8): assert os.path.islink(self.test_rc.version("privkey", i)) assert "privkey3.pem" == os.path.basename(filesystem.readlink( self.test_rc.version("privkey", i))) for kind in ALL_FOUR: assert self.test_rc.available_versions(kind) == list(range(1, 9)) assert self.test_rc.current_version(kind) == 3 # Test updating from latest version rather than old version self.test_rc.update_all_links_to(8) assert 9 == self.test_rc.save_successor(8, b'last', None, b'attempt', self.config) for kind in ALL_FOUR: assert self.test_rc.available_versions(kind) == \ list(range(1, 10)) assert self.test_rc.current_version(kind) == 8 with open(self.test_rc.version("fullchain", 9)) as f: assert f.read() == "last" + "attempt" temp_config_file = os.path.join(self.config.renewal_configs_dir, self.test_rc.lineagename) + ".conf.new" with open(temp_config_file, "w") as f: f.write("We previously crashed while writing me :(") # Test updating when providing a new privkey. The key should # be saved in a new file rather than creating a new symlink. assert 10 == self.test_rc.save_successor(9, b'with', b'a', b'key', self.config) assert os.path.exists(self.test_rc.version("privkey", 10)) assert not os.path.islink(self.test_rc.version("privkey", 10)) assert not os.path.exists(temp_config_file) @test_util.skip_on_windows('Group/everybody permissions are not maintained on Windows.') @mock.patch("certbot._internal.storage.relevant_values") def test_save_successor_maintains_group_mode(self, mock_rv): # Mock relevant_values() to claim that all values are relevant here # (to avoid instantiating parser) mock_rv.side_effect = lambda x: x.to_dict() for kind in ALL_FOUR: self._write_out_kind(kind, 1) self.test_rc.update_all_links_to(1) assert filesystem.check_mode(self.test_rc.version("privkey", 1), 0o600) filesystem.chmod(self.test_rc.version("privkey", 1), 0o444) # If no new key, permissions should be the same (we didn't write any keys) self.test_rc.save_successor(1, b"newcert", None, b"new chain", self.config) assert filesystem.check_mode(self.test_rc.version("privkey", 2), 0o444) # If new key, permissions should be kept as 644 self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new chain", self.config) assert filesystem.check_mode(self.test_rc.version("privkey", 3), 0o644) # If permissions reverted, next renewal will also revert permissions of new key filesystem.chmod(self.test_rc.version("privkey", 3), 0o400) self.test_rc.save_successor(3, b"newcert", b"new_privkey", b"new chain", self.config) assert filesystem.check_mode(self.test_rc.version("privkey", 4), 0o600) @mock.patch("certbot._internal.storage.relevant_values") @mock.patch("certbot._internal.storage.filesystem.copy_ownership_and_apply_mode") def test_save_successor_maintains_gid(self, mock_ownership, mock_rv): # Mock relevant_values() to claim that all values are relevant here # (to avoid instantiating parser) mock_rv.side_effect = lambda x: x.to_dict() for kind in ALL_FOUR: self._write_out_kind(kind, 1) self.test_rc.update_all_links_to(1) self.test_rc.save_successor(1, b"newcert", None, b"new chain", self.config) assert mock_ownership.called is False self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new chain", self.config) assert mock_ownership.called @mock.patch("certbot._internal.storage.relevant_values") def test_new_lineage(self, mock_rv): """Test for new_lineage() class method.""" # Mock relevant_values to say everything is relevant here (so we # don't have to mock the parser to help it decide!) mock_rv.side_effect = lambda x: x.to_dict() from certbot._internal import storage result = storage.RenewableCert.new_lineage( "the-lineage.com", b"cert", b"privkey", b"chain", self.config) # This consistency check tests most relevant properties about the # newly created cert lineage. # pylint: disable=protected-access assert result._consistent() assert os.path.exists(os.path.join( self.config.renewal_configs_dir, "the-lineage.com.conf")) assert os.path.exists(os.path.join( self.config.live_dir, "README")) assert os.path.exists(os.path.join( self.config.live_dir, "the-lineage.com", "README")) assert filesystem.check_mode(result.key_path, 0o600) with open(result.fullchain, "rb") as f: assert f.read() == b"cert" + b"chain" # Let's do it again and make sure it makes a different lineage result = storage.RenewableCert.new_lineage( "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config) assert os.path.exists(os.path.join( self.config.renewal_configs_dir, "the-lineage.com-0001.conf")) assert os.path.exists(os.path.join( self.config.live_dir, "the-lineage.com-0001", "README")) # Allow write to existing but empty dir filesystem.mkdir(os.path.join(self.config.default_archive_dir, "the-lineage.com-0002")) result = storage.RenewableCert.new_lineage( "the-lineage.com", b"cert3", b"privkey3", b"chain3", self.config) assert os.path.exists(os.path.join( self.config.live_dir, "the-lineage.com-0002", "README")) assert filesystem.check_mode(result.key_path, 0o600) # Now trigger the detection of already existing files shutil.copytree(os.path.join(self.config.live_dir, "the-lineage.com"), os.path.join(self.config.live_dir, "the-lineage.com-0003")) with pytest.raises(errors.CertStorageError): storage.RenewableCert.new_lineage("the-lineage.com", b"cert4", b"privkey4", b"chain4", self.config) shutil.copytree(os.path.join(self.config.live_dir, "the-lineage.com"), os.path.join(self.config.live_dir, "other-example.com")) with pytest.raises(errors.CertStorageError): storage.RenewableCert.new_lineage("other-example.com", b"cert5", b"privkey5", b"chain5", self.config) # Make sure it can accept renewal parameters result = storage.RenewableCert.new_lineage( "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config) # TODO: Conceivably we could test that the renewal parameters actually # got saved @mock.patch("certbot._internal.storage.relevant_values") def test_new_lineage_nonexistent_dirs(self, mock_rv): """Test that directories can be created if they don't exist.""" # Mock relevant_values to say everything is relevant here (so we # don't have to mock the parser to help it decide!) mock_rv.side_effect = lambda x: x.to_dict() from certbot._internal import storage shutil.rmtree(self.config.renewal_configs_dir) shutil.rmtree(self.config.default_archive_dir) shutil.rmtree(self.config.live_dir) storage.RenewableCert.new_lineage( "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config) assert os.path.exists( os.path.join( self.config.renewal_configs_dir, "the-lineage.com.conf")) assert os.path.exists(os.path.join( self.config.live_dir, "the-lineage.com", "privkey.pem")) assert os.path.exists(os.path.join( self.config.default_archive_dir, "the-lineage.com", "privkey1.pem")) @mock.patch("certbot._internal.storage.util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): from certbot._internal import storage mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" with pytest.raises(errors.CertStorageError): storage.RenewableCert.new_lineage("example.com", "cert", "privkey", "chain", self.config) def test_bad_kind(self): with pytest.raises(errors.CertStorageError): self.test_rc.current_target("elephant") with pytest.raises(errors.CertStorageError): self.test_rc.current_version("elephant") with pytest.raises(errors.CertStorageError): self.test_rc.version("elephant", 17) with pytest.raises(errors.CertStorageError): self.test_rc.available_versions("elephant") with pytest.raises(errors.CertStorageError): self.test_rc.newest_available_version("elephant") # pylint: disable=protected-access with pytest.raises(errors.CertStorageError): self.test_rc._update_link_to("elephant", 17) @mock.patch("certbot.ocsp.RevocationChecker.ocsp_revoked_by_paths") def test_ocsp_revoked(self, mock_checker): # Write out test files for kind in ALL_FOUR: self._write_out_kind(kind, 1) version = self.test_rc.latest_common_version() expected_cert_path = self.test_rc.version("cert", version) expected_chain_path = self.test_rc.version("chain", version) # Test with cert revoked mock_checker.return_value = True assert self.test_rc.ocsp_revoked(version) assert mock_checker.call_args[0][0] == expected_cert_path assert mock_checker.call_args[0][1] == expected_chain_path # Test with cert not revoked mock_checker.return_value = False assert not self.test_rc.ocsp_revoked(version) assert mock_checker.call_args[0][0] == expected_cert_path assert mock_checker.call_args[0][1] == expected_chain_path # Test with error mock_checker.side_effect = ValueError with mock.patch("certbot._internal.storage.logger.warning") as logger: assert not self.test_rc.ocsp_revoked(version) assert mock_checker.call_args[0][0] == expected_cert_path assert mock_checker.call_args[0][1] == expected_chain_path log_msg = logger.call_args[0][0] assert "An error occurred determining the OCSP status" in log_msg def test_add_time_interval(self): from certbot._internal import storage # this month has 30 days, and the next year is a leap year time_1 = datetime.datetime(2003, 11, 20, 11, 59, 21, tzinfo=pytz.UTC) # this month has 31 days, and the next year is not a leap year time_2 = datetime.datetime(2012, 10, 18, 21, 31, 16, tzinfo=pytz.UTC) # in different time zone (GMT+8) time_3 = pytz.timezone('Asia/Shanghai').fromutc( datetime.datetime(2015, 10, 26, 22, 25, 41)) intended = { (time_1, ""): time_1, (time_2, ""): time_2, (time_3, ""): time_3, (time_1, "17 days"): time_1 + datetime.timedelta(17), (time_2, "17 days"): time_2 + datetime.timedelta(17), (time_1, "30"): time_1 + datetime.timedelta(30), (time_2, "30"): time_2 + datetime.timedelta(30), (time_1, "7 weeks"): time_1 + datetime.timedelta(49), (time_2, "7 weeks"): time_2 + datetime.timedelta(49), # 1 month is always 30 days, no matter which month it is (time_1, "1 month"): time_1 + datetime.timedelta(30), (time_2, "1 month"): time_2 + datetime.timedelta(31), # 1 year could be 365 or 366 days, depends on the year (time_1, "1 year"): time_1 + datetime.timedelta(366), (time_2, "1 year"): time_2 + datetime.timedelta(365), (time_1, "1 year 1 day"): time_1 + datetime.timedelta(367), (time_2, "1 year 1 day"): time_2 + datetime.timedelta(366), (time_1, "1 year-1 day"): time_1 + datetime.timedelta(365), (time_2, "1 year-1 day"): time_2 + datetime.timedelta(364), (time_1, "4 years"): time_1 + datetime.timedelta(1461), (time_2, "4 years"): time_2 + datetime.timedelta(1461), } for parameters, excepted in intended.items(): base_time, interval = parameters assert storage.add_time_interval(base_time, interval) == \ excepted def test_server(self): self.test_rc.configuration["renewalparams"] = {} assert self.test_rc.server is None rp = self.test_rc.configuration["renewalparams"] rp["server"] = "https://acme.example/dir" assert self.test_rc.server == "https://acme.example/dir" def test_is_test_cert(self): self.test_rc.configuration["renewalparams"] = {} rp = self.test_rc.configuration["renewalparams"] assert self.test_rc.is_test_cert is False rp["server"] = "https://acme-staging-v02.api.letsencrypt.org/directory" assert self.test_rc.is_test_cert is True rp["server"] = "https://staging.someotherca.com/directory" assert self.test_rc.is_test_cert is True rp["server"] = "https://acme-v01.api.letsencrypt.org/directory" assert self.test_rc.is_test_cert is False rp["server"] = "https://acme-v02.api.letsencrypt.org/directory" assert self.test_rc.is_test_cert is False def test_missing_cert(self): from certbot._internal import storage with pytest.raises(errors.CertStorageError): storage.RenewableCert(self.config_file.filename, self.config) os.symlink("missing", self.config_file[ALL_FOUR[0]]) with pytest.raises(errors.CertStorageError): storage.RenewableCert(self.config_file.filename, self.config) def test_write_renewal_config(self): # Mostly tested by the process of creating and updating lineages, # but we can test that this successfully creates files, removes # unneeded items, and preserves comments. temp = os.path.join(self.config.config_dir, "sample-file") temp2 = os.path.join(self.config.config_dir, "sample-file.new") with open(temp, "w") as f: f.write("[renewalparams]\nuseful = value # A useful value\n" "useless = value # Not needed\n") filesystem.chmod(temp, 0o640) target = {} for x in ALL_FOUR: target[x] = "somewhere" archive_dir = "the_archive" relevant_data = {"useful": "new_value"} from certbot._internal import storage storage.write_renewal_config(temp, temp2, archive_dir, target, relevant_data) with open(temp2, "r") as f: content = f.read() # useful value was updated assert "useful = new_value" in content # associated comment was preserved assert "A useful value" in content # useless value was deleted assert "useless" not in content # check version was stored assert "version = {0}".format(certbot.__version__) in content # ensure permissions are copied assert stat.S_IMODE(os.lstat(temp).st_mode) == \ stat.S_IMODE(os.lstat(temp2).st_mode) def test_update_symlinks(self): from certbot._internal import storage archive_dir_path = os.path.join(self.config.config_dir, "archive", "example.org") for kind in ALL_FOUR: live_path = self.config_file[kind] basename = kind + "1.pem" archive_path = os.path.join(archive_dir_path, basename) open(archive_path, 'a').close() os.symlink(os.path.join(self.config.config_dir, basename), live_path) with pytest.raises(errors.CertStorageError): storage.RenewableCert(self.config_file.filename, self.config) storage.RenewableCert(self.config_file.filename, self.config, update_symlinks=True) def test_truncate(self): # It should not do anything when there's less than 5 cert history for kind in ALL_FOUR: self._write_out_kind(kind, 1) with mock.patch('certbot.compat.os.unlink') as mock_unlink: self.test_rc.truncate() mock_unlink.assert_not_called() # It should truncate the excess when there's more than 5 cert history for kind in ALL_FOUR: for i in range(2, 8): self._write_out_kind(kind, i) with mock.patch('certbot.compat.os.unlink') as mock_unlink: self.test_rc.truncate() assert mock_unlink.call_count == 1 * len(ALL_FOUR) assert "1.pem" in mock_unlink.call_args_list[0][0][0] class DeleteFilesTest(BaseRenewableCertTest): """Tests for certbot._internal.storage.delete_files""" def setUp(self): super().setUp() for kind in ALL_FOUR: kind_path = os.path.join(self.config.config_dir, "live", "example.org", kind + ".pem") with open(kind_path, 'a'): pass self.config_file.write() assert os.path.exists(os.path.join( self.config.renewal_configs_dir, "example.org.conf")) assert os.path.exists(os.path.join( self.config.live_dir, "example.org")) assert os.path.exists(os.path.join( self.config.config_dir, "archive", "example.org")) def _call(self): from certbot._internal import storage with mock.patch("certbot._internal.storage.logger"): storage.delete_files(self.config, "example.org") def test_delete_all_files(self): self._call() assert not os.path.exists(os.path.join( self.config.renewal_configs_dir, "example.org.conf")) assert not os.path.exists(os.path.join( self.config.live_dir, "example.org")) assert not os.path.exists(os.path.join( self.config.config_dir, "archive", "example.org")) def test_bad_renewal_config(self): with open(self.config_file.filename, 'a') as config_file: config_file.write("asdfasfasdfasdf") with pytest.raises(errors.CertStorageError): self._call() assert os.path.exists(os.path.join( self.config.live_dir, "example.org")) assert not os.path.exists(os.path.join( self.config.renewal_configs_dir, "example.org.conf")) def test_no_renewal_config(self): os.remove(self.config_file.filename) with pytest.raises(errors.CertStorageError): self._call() assert os.path.exists(os.path.join( self.config.live_dir, "example.org")) assert not os.path.exists(self.config_file.filename) def test_no_cert_file(self): os.remove(os.path.join( self.config.live_dir, "example.org", "cert.pem")) self._call() assert not os.path.exists(self.config_file.filename) assert not os.path.exists(os.path.join( self.config.live_dir, "example.org")) assert not os.path.exists(os.path.join( self.config.config_dir, "archive", "example.org")) def test_no_readme_file(self): os.remove(os.path.join( self.config.live_dir, "example.org", "README")) self._call() assert not os.path.exists(self.config_file.filename) assert not os.path.exists(os.path.join( self.config.live_dir, "example.org")) assert not os.path.exists(os.path.join( self.config.config_dir, "archive", "example.org")) def test_livedir_not_empty(self): with open(os.path.join( self.config.live_dir, "example.org", "other_file"), 'a'): pass self._call() assert not os.path.exists(self.config_file.filename) assert os.path.exists(os.path.join( self.config.live_dir, "example.org")) assert not os.path.exists(os.path.join( self.config.config_dir, "archive", "example.org")) def test_no_archive(self): archive_dir = os.path.join(self.config.config_dir, "archive", "example.org") os.rmdir(archive_dir) self._call() assert not os.path.exists(self.config_file.filename) assert not os.path.exists(os.path.join( self.config.live_dir, "example.org")) assert not os.path.exists(archive_dir) class CertPathForCertNameTest(BaseRenewableCertTest): """Test for certbot._internal.storage.cert_path_for_cert_name""" def setUp(self): super().setUp() self.config_file.write() self._write_out_ex_kinds() self.fullchain = os.path.join(self.config.config_dir, 'live', 'example.org', 'fullchain.pem') self.config.cert_path = self.fullchain def _call(self, cli_config, certname): from certbot._internal.storage import cert_path_for_cert_name return cert_path_for_cert_name(cli_config, certname) def test_simple_cert_name(self): assert self._call(self.config, 'example.org') == self.fullchain def test_no_such_cert_name(self): with pytest.raises(errors.CertStorageError): self._call(self.config, 'fake-example.org') if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/tests/util_test.py0000664000175100017510000006027214561227515021344 0ustar00ericaerica"""Tests for certbot.util.""" import argparse import errno from importlib import reload as reload_module import io import sys import unittest from unittest import mock import pytest from certbot import errors from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util class EnvNoSnapForExternalCallsTest(unittest.TestCase): """Tests for certbot.util.env_no_snap_for_external_calls.""" @classmethod def _call(cls): from certbot.util import env_no_snap_for_external_calls return env_no_snap_for_external_calls() def test_removed(self): original_path = os.environ['PATH'] env_copy_dict = os.environ.copy() env_copy_dict['PATH'] = 'RANDOM_NONSENSE_GARBAGE/blah/blah:' + original_path env_copy_dict['SNAP'] = 'RANDOM_NONSENSE_GARBAGE' env_copy_dict['CERTBOT_SNAPPED'] = 'True' with mock.patch('certbot.compat.os.environ.copy', return_value=env_copy_dict): assert self._call()['PATH'] == original_path def test_noop(self): env_copy_dict_unmodified = os.environ.copy() env_copy_dict_unmodified['PATH'] = 'RANDOM_NONSENSE_GARBAGE/blah/blah:' \ + env_copy_dict_unmodified['PATH'] env_copy_dict = env_copy_dict_unmodified.copy() with mock.patch('certbot.compat.os.environ.copy', return_value=env_copy_dict): # contains neither necessary key env_copy_dict.pop('SNAP', None) env_copy_dict.pop('CERTBOT_SNAPPED', None) assert self._call()['PATH'] == env_copy_dict_unmodified['PATH'] # contains only one necessary key env_copy_dict['SNAP'] = 'RANDOM_NONSENSE_GARBAGE' assert self._call()['PATH'] == env_copy_dict_unmodified['PATH'] del env_copy_dict['SNAP'] env_copy_dict['CERTBOT_SNAPPED'] = 'True' assert self._call()['PATH'] == env_copy_dict_unmodified['PATH'] class RunScriptTest(unittest.TestCase): """Tests for certbot.util.run_script.""" @classmethod def _call(cls, params): from certbot.util import run_script return run_script(params) @mock.patch("certbot.util.subprocess.run") def test_default(self, mock_run): """These will be changed soon enough with reload.""" mock_run().returncode = 0 mock_run().stdout = "stdout" mock_run().stderr = "stderr" out, err = self._call(["test"]) assert out == "stdout" assert err == "stderr" @mock.patch("certbot.util.subprocess.run") def test_bad_process(self, mock_run): mock_run.side_effect = OSError with pytest.raises(errors.SubprocessError): self._call(["test"]) @mock.patch("certbot.util.subprocess.run") def test_failure(self, mock_run): mock_run().returncode = 1 with pytest.raises(errors.SubprocessError): self._call(["test"]) class ExeExistsTest(unittest.TestCase): """Tests for certbot.util.exe_exists.""" @classmethod def _call(cls, exe): from certbot.util import exe_exists return exe_exists(exe) def test_exe_exists(self): with mock.patch("certbot.util.filesystem.is_executable", return_value=True): assert self._call("/path/to/exe") def test_exe_not_exists(self): with mock.patch("certbot.util.filesystem.is_executable", return_value=False): assert not self._call("/path/to/exe") class LockDirUntilExit(test_util.TempDirTestCase): """Tests for certbot.util.lock_dir_until_exit.""" @classmethod def _call(cls, *args, **kwargs): from certbot.util import lock_dir_until_exit return lock_dir_until_exit(*args, **kwargs) def setUp(self): super().setUp() # reset global state from other tests import certbot.util reload_module(certbot.util) @mock.patch('certbot.util.logger') @mock.patch('certbot.util.atexit_register') def test_it(self, mock_register, mock_logger): subdir = os.path.join(self.tempdir, 'subdir') filesystem.mkdir(subdir) self._call(self.tempdir) self._call(subdir) self._call(subdir) assert mock_register.call_count == 1 registered_func = mock_register.call_args[0][0] from certbot import util # Despite lock_dir_until_exit has been called twice to subdir, its lock should have been # added only once. So we expect to have two lock references: for self.tempdir and subdir assert len(util._LOCKS) == 2 # pylint: disable=protected-access registered_func() # Exception should not be raised # Logically, logger.debug, that would be invoked in case of unlock failure, # should never been called. assert mock_logger.debug.call_count == 0 class SetUpCoreDirTest(test_util.TempDirTestCase): """Tests for certbot.util.make_or_verify_core_dir.""" def _call(self, *args, **kwargs): from certbot.util import set_up_core_dir return set_up_core_dir(*args, **kwargs) @mock.patch('certbot.util.lock_dir_until_exit') def test_success(self, mock_lock): new_dir = os.path.join(self.tempdir, 'new') self._call(new_dir, 0o700, False) assert os.path.exists(new_dir) assert mock_lock.call_count == 1 @mock.patch('certbot.util.make_or_verify_dir') def test_failure(self, mock_make_or_verify): mock_make_or_verify.side_effect = OSError with pytest.raises(errors.Error): self._call(self.tempdir, 0o700, False) class MakeOrVerifyDirTest(test_util.TempDirTestCase): """Tests for certbot.util.make_or_verify_dir. Note that it is not possible to test for a wrong directory owner, as this testing script would have to be run as root. """ def setUp(self): super().setUp() self.path = os.path.join(self.tempdir, "foo") filesystem.mkdir(self.path, 0o600) def _call(self, directory, mode): from certbot.util import make_or_verify_dir return make_or_verify_dir(directory, mode, strict=True) def test_creates_dir_when_missing(self): path = os.path.join(self.tempdir, "bar") self._call(path, 0o650) assert os.path.isdir(path) assert filesystem.check_mode(path, 0o650) def test_existing_correct_mode_does_not_fail(self): self._call(self.path, 0o600) assert filesystem.check_mode(self.path, 0o600) def test_existing_wrong_mode_fails(self): with pytest.raises(errors.Error): self._call(self.path, 0o400) def test_reraises_os_error(self): with mock.patch.object(filesystem, "makedirs") as makedirs: makedirs.side_effect = OSError() with pytest.raises(OSError): self._call("bar", 12312312) class UniqueFileTest(test_util.TempDirTestCase): """Tests for certbot.util.unique_file.""" def setUp(self): super().setUp() self.default_name = os.path.join(self.tempdir, "foo.txt") def _call(self, mode=0o600): from certbot.util import unique_file return unique_file(self.default_name, mode) def test_returns_fd_for_writing(self): fd, name = self._call() fd.write("bar") fd.close() with open(name) as f: assert f.read() == "bar" def test_right_mode(self): fd1, name1 = self._call(0o700) fd2, name2 = self._call(0o600) assert filesystem.check_mode(name1, 0o700) assert filesystem.check_mode(name2, 0o600) fd1.close() fd2.close() def test_default_exists(self): fd1, name1 = self._call() # create 0000_foo.txt fd2, name2 = self._call() fd3, name3 = self._call() assert name1 != name2 assert name1 != name3 assert name2 != name3 assert os.path.dirname(name1) == self.tempdir assert os.path.dirname(name2) == self.tempdir assert os.path.dirname(name3) == self.tempdir basename1 = os.path.basename(name2) assert basename1.endswith("foo.txt") basename2 = os.path.basename(name2) assert basename2.endswith("foo.txt") basename3 = os.path.basename(name3) assert basename3.endswith("foo.txt") fd1.close() fd2.close() fd3.close() class UniqueLineageNameTest(test_util.TempDirTestCase): """Tests for certbot.util.unique_lineage_name.""" def _call(self, filename, mode=0o777): from certbot.util import unique_lineage_name return unique_lineage_name(self.tempdir, filename, mode) def test_basic(self): f, path = self._call("wow") assert isinstance(f, io.TextIOWrapper) assert os.path.join(self.tempdir, "wow.conf") == path f.close() def test_multiple(self): items = [] for _ in range(10): items.append(self._call("wow")) f, name = items[-1] assert isinstance(f, io.TextIOWrapper) assert isinstance(name, str) assert "wow-0009.conf" in name for f, _ in items: f.close() def test_failure(self): with mock.patch("certbot.compat.filesystem.open", side_effect=OSError(errno.EIO)): with pytest.raises(OSError): self._call("wow") class SafelyRemoveTest(test_util.TempDirTestCase): """Tests for certbot.util.safely_remove.""" def setUp(self): super().setUp() self.path = os.path.join(self.tempdir, "foo") def _call(self): from certbot.util import safely_remove return safely_remove(self.path) def test_exists(self): with open(self.path, "w"): pass # just create the file self._call() assert not os.path.exists(self.path) def test_missing(self): self._call() # no error, yay! assert not os.path.exists(self.path) def test_other_error_passthrough(self): with mock.patch("certbot.util.os.remove") as mock_remove: mock_remove.side_effect = OSError with pytest.raises(OSError): self._call() class SafeEmailTest(unittest.TestCase): """Test safe_email.""" @classmethod def _call(cls, addr): from certbot.util import safe_email return safe_email(addr) def test_valid_emails(self): addrs = [ "certbot@certbot.org", "tbd.ade@gmail.com", "abc_def.jdk@hotmail.museum", ] for addr in addrs: assert self._call(addr), "%s failed." % addr def test_invalid_emails(self): addrs = [ "certbot@certbot..org", ".tbd.ade@gmail.com", "~/abc_def.jdk@hotmail.museum", ] for addr in addrs: assert not self._call(addr), "%s failed." % addr class AddDeprecatedArgumentTest(unittest.TestCase): """Test add_deprecated_argument.""" def setUp(self): self.parser = argparse.ArgumentParser() def _call(self, argument_name, nargs): from certbot.util import add_deprecated_argument add_deprecated_argument(self.parser.add_argument, argument_name, nargs) def test_warning_no_arg(self): self._call("--old-option", 0) with mock.patch("certbot.util.logger.warning") as mock_warn: self.parser.parse_args(["--old-option"]) assert mock_warn.call_count == 1 assert "is deprecated" in mock_warn.call_args[0][0] assert "--old-option" in mock_warn.call_args[0][1] def test_warning_with_arg(self): self._call("--old-option", 1) with mock.patch("certbot.util.logger.warning") as mock_warn: self.parser.parse_args(["--old-option", "42"]) assert mock_warn.call_count == 1 assert "is deprecated" in mock_warn.call_args[0][0] assert "--old-option" in mock_warn.call_args[0][1] def test_help(self): self._call("--old-option", 2) stdout = io.StringIO() with mock.patch("sys.stdout", new=stdout): try: self.parser.parse_args(["-h"]) except SystemExit: pass assert "--old-option" not in stdout.getvalue() def test_set_constant(self): """Test when ACTION_TYPES_THAT_DONT_NEED_A_VALUE is a set. This variable is a set in configargparse versions < 0.12.0. """ self._test_constant_common(set) def test_tuple_constant(self): """Test when ACTION_TYPES_THAT_DONT_NEED_A_VALUE is a tuple. This variable is a tuple in configargparse versions >= 0.12.0. """ self._test_constant_common(tuple) def _test_constant_common(self, typ): with mock.patch("certbot.util.configargparse") as mock_configargparse: mock_configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE = typ() self._call("--old-option", 1) self._call("--old-option2", 2) assert len(mock_configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE) == 1 class EnforceLeValidity(unittest.TestCase): """Test enforce_le_validity.""" def _call(self, domain): from certbot.util import enforce_le_validity return enforce_le_validity(domain) def test_sanity(self): with pytest.raises(errors.ConfigurationError): self._call(u"..") def test_invalid_chars(self): with pytest.raises(errors.ConfigurationError): self._call(u"hello_world.example.com") def test_leading_hyphen(self): with pytest.raises(errors.ConfigurationError): self._call(u"-a.example.com") def test_trailing_hyphen(self): with pytest.raises(errors.ConfigurationError): self._call(u"a-.example.com") def test_one_label(self): with pytest.raises(errors.ConfigurationError): self._call(u"com") def test_valid_domain(self): assert self._call(u"example.com") == u"example.com" def test_input_with_scheme(self): with pytest.raises(errors.ConfigurationError): self._call(u"http://example.com") with pytest.raises(errors.ConfigurationError): self._call(u"https://example.com") def test_valid_input_with_scheme_name(self): assert self._call(u"http.example.com") == u"http.example.com" class EnforceDomainSanityTest(unittest.TestCase): """Test enforce_domain_sanity.""" def _call(self, domain): from certbot.util import enforce_domain_sanity return enforce_domain_sanity(domain) def test_nonascii_str(self): with pytest.raises(errors.ConfigurationError): self._call(u"eichh\u00f6rnchen.example.com".encode("utf-8")) def test_nonascii_unicode(self): with pytest.raises(errors.ConfigurationError): self._call(u"eichh\u00f6rnchen.example.com") def test_too_long(self): long_domain = u"a"*256 with pytest.raises(errors.ConfigurationError): self._call(long_domain) def test_not_too_long(self): not_too_long_domain = u"{0}.{1}.{2}.{3}".format("a"*63, "b"*63, "c"*63, "d"*63) self._call(not_too_long_domain) def test_empty_label(self): empty_label_domain = u"fizz..example.com" with pytest.raises(errors.ConfigurationError): self._call(empty_label_domain) def test_empty_trailing_label(self): empty_trailing_label_domain = u"example.com.." with pytest.raises(errors.ConfigurationError): self._call(empty_trailing_label_domain) def test_long_label_1(self): long_label_domain = u"a"*64 with pytest.raises(errors.ConfigurationError): self._call(long_label_domain) def test_long_label_2(self): long_label_domain = u"{0}.{1}.com".format(u"a"*64, u"b"*63) with pytest.raises(errors.ConfigurationError): self._call(long_label_domain) def test_not_long_label(self): not_too_long_label_domain = u"{0}.{1}.com".format(u"a"*63, u"b"*63) self._call(not_too_long_label_domain) def test_empty_domain(self): empty_domain = u"" with pytest.raises(errors.ConfigurationError): self._call(empty_domain) def test_punycode_ok(self): # Punycode is now legal, so no longer an error; instead check # that it's _not_ an error (at the initial sanity check stage) self._call('this.is.xn--ls8h.tld') class IsWildcardDomainTest(unittest.TestCase): """Tests for is_wildcard_domain.""" def setUp(self): self.wildcard = u"*.example.org" self.no_wildcard = u"example.org" def _call(self, domain): from certbot.util import is_wildcard_domain return is_wildcard_domain(domain) def test_no_wildcard(self): assert not self._call(self.no_wildcard) assert not self._call(self.no_wildcard.encode()) def test_wildcard(self): assert self._call(self.wildcard) assert self._call(self.wildcard.encode()) class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" @mock.patch("certbot.util.distro") @unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux") def test_systemd_os_release_like(self, m_distro): import certbot.util as cbutil m_distro.like.return_value = "first debian third" id_likes = cbutil.get_systemd_os_like() assert len(id_likes) == 3 assert "debian" in id_likes @mock.patch("certbot.util.distro") @unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux") def test_get_os_info_ua(self, m_distro): import certbot.util as cbutil with mock.patch('platform.system_alias', return_value=('linux', '42', '42')): m_distro.version.return_value = "1.0" # empty value on first call for fallback to "get_python_os_info" in get_os_info_ua m_distro.name.side_effect = ["", "something", "something"] assert cbutil.get_os_info_ua() == \ " ".join(cbutil.get_python_os_info(pretty=True)) m_distro.name.side_effect = ["whatever"] assert cbutil.get_os_info_ua() == "whatever" @mock.patch("certbot.util.distro") @unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux") def test_get_os_info(self, m_distro): import certbot.util as cbutil with mock.patch("platform.system") as mock_platform: m_distro.id.return_value = "name" m_distro.version.return_value = "version" mock_platform.return_value = "linux" assert cbutil.get_os_info() == ("name", "version") m_distro.id.return_value = "something" m_distro.version.return_value = "else" assert cbutil.get_os_info() == ("something", "else") def test_non_systemd_os_info(self): import certbot.util as cbutil with mock.patch('certbot.util._USE_DISTRO', False): with mock.patch('platform.system_alias', return_value=('NonSystemD', '42', '42')): assert cbutil.get_python_os_info()[0] == 'nonsystemd' with mock.patch('platform.system_alias', return_value=('darwin', '', '')): with mock.patch("subprocess.run") as run_mock: run_mock().stdout = '42.42.42' assert cbutil.get_python_os_info()[0] == 'darwin' assert cbutil.get_python_os_info()[1] == '42.42.42' with mock.patch('platform.system_alias', return_value=('freebsd', '9.3-RC3-p1', '')): assert cbutil.get_python_os_info() == ("freebsd", "9") with mock.patch('platform.system_alias', return_value=('windows', '', '')): with mock.patch('platform.win32_ver', return_value=('4242', '95', '2', '')): assert cbutil.get_python_os_info() == \ ("windows", "95") @mock.patch("certbot.util.distro") @unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux") def test_python_os_info_notfound(self, m_distro): import certbot.util as cbutil m_distro.id.return_value = "" m_distro.version.return_value = "" assert cbutil.get_python_os_info()[0] == "linux" @mock.patch("certbot.util.distro") @unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux") def test_python_os_info_custom(self, m_distro): import certbot.util as cbutil m_distro.id.return_value = "testdist" m_distro.version.return_value = "42" assert cbutil.get_python_os_info() == ("testdist", "42") class AtexitRegisterTest(unittest.TestCase): """Tests for certbot.util.atexit_register.""" def setUp(self): self.func = mock.MagicMock() self.args = ('hi',) self.kwargs = {'answer': 42} @classmethod def _call(cls, *args, **kwargs): from certbot.util import atexit_register return atexit_register(*args, **kwargs) def test_called(self): self._test_common(os.getpid()) self.func.assert_called_with(*self.args, **self.kwargs) def test_not_called(self): self._test_common(initial_pid=-1) assert self.func.called is False def _test_common(self, initial_pid): with mock.patch('certbot.util._INITIAL_PID', initial_pid): with mock.patch('certbot.util.atexit') as mock_atexit: self._call(self.func, *self.args, **self.kwargs) # _INITIAL_PID must be mocked when calling atexit_func assert mock_atexit.register.called args, kwargs = mock_atexit.register.call_args atexit_func = args[0] atexit_func(*args[1:], **kwargs) class LooseVersionTest(unittest.TestCase): """Test for certbot.util.LooseVersion. These tests are based on the original tests for distutils.version.LooseVersion at https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/tests/test_version.py#L58-L81. """ @classmethod def _call(cls, *args, **kwargs): from certbot.util import LooseVersion return LooseVersion(*args, **kwargs) def test_less_than(self): comparisons = (('1.5.1', '1.5.2b2'), ('3.4j', '1996.07.12'), ('2g6', '11g'), ('0.960923', '2.2beta29'), ('1.13++', '5.5.kw'), ('2.0', '2.0.1'), ('a', 'b')) for v1, v2 in comparisons: assert self._call(v1).try_risky_comparison(self._call(v2)) == -1 def test_equal(self): comparisons = (('8.02', '8.02'), ('1a', '1a'), ('2', '2.0.0'), ('2.0', '2.0.0')) for v1, v2 in comparisons: assert self._call(v1).try_risky_comparison(self._call(v2)) == 0 def test_greater_than(self): comparisons = (('161', '3.10a'), ('3.2.pl0', '3.1.1.6')) for v1, v2 in comparisons: assert self._call(v1).try_risky_comparison(self._call(v2)) == 1 def test_incomparible(self): comparisons = (('bookworm/sid', '9'), ('1a', '1.0')) for v1, v2 in comparisons: with pytest.raises(ValueError): assert self._call(v1).try_risky_comparison(self._call(v2)) class ParseLooseVersionTest(unittest.TestCase): """Test for certbot.util.parse_loose_version. These tests are based on the original tests for distutils.version.LooseVersion at https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/tests/test_version.py#L58-L81. """ @classmethod def _call(cls, *args, **kwargs): from certbot.util import parse_loose_version return parse_loose_version(*args, **kwargs) def test_less_than(self): comparisons = (('1.5.1', '1.5.2b2'), ('3.4j', '1996.07.12'), ('2g6', '11g'), ('0.960923', '2.2beta29'), ('1.13++', '5.5.kw')) for v1, v2 in comparisons: assert self._call(v1) < self._call(v2) def test_equal(self): assert self._call('8.02') == self._call('8.02') def test_greater_than(self): comparisons = (('161', '3.10a'), ('3.2.pl0', '3.1.1.6')) for v1, v2 in comparisons: assert self._call(v1) > self._call(v2) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/_internal/updater.py0000664000175100017510000001121314561227515017621 0ustar00ericaerica"""Updaters run at renewal""" import logging from certbot import configuration from certbot import errors from certbot import interfaces from certbot._internal import storage from certbot._internal.plugins import disco as plugin_disco from certbot._internal.plugins import selection as plug_sel from certbot.plugins import enhancements logger = logging.getLogger(__name__) def run_generic_updaters(config: configuration.NamespaceConfig, lineage: storage.RenewableCert, plugins: plugin_disco.PluginsRegistry) -> None: """Run updaters that the plugin supports :param config: Configuration object :type config: certbot.configuration.NamespaceConfig :param lineage: Certificate lineage object :type lineage: storage.RenewableCert :param plugins: List of plugins :type plugins: certbot._internal.plugins.disco.PluginsRegistry :returns: `None` :rtype: None """ if config.dry_run: logger.debug("Skipping updaters in dry-run mode.") return try: installer = plug_sel.get_unprepared_installer(config, plugins) except errors.Error as e: logger.error("Could not choose appropriate plugin for updaters: %s", e) return if installer: _run_updaters(lineage, installer, config) _run_enhancement_updaters(lineage, installer, config) def run_renewal_deployer(config: configuration.NamespaceConfig, lineage: storage.RenewableCert, installer: interfaces.Installer) -> None: """Helper function to run deployer interface method if supported by the used installer plugin. :param config: Configuration object :type config: certbot.configuration.NamespaceConfig :param lineage: Certificate lineage object :type lineage: storage.RenewableCert :param installer: Installer object :type installer: interfaces.Installer :returns: `None` :rtype: None """ if config.dry_run: logger.debug("Skipping renewal deployer in dry-run mode.") return if not config.disable_renew_updates and isinstance(installer, interfaces.RenewDeployer): installer.renew_deploy(lineage) _run_enhancement_deployers(lineage, installer, config) def _run_updaters(lineage: storage.RenewableCert, installer: interfaces.Installer, config: configuration.NamespaceConfig) -> None: """Helper function to run the updater interface methods if supported by the used installer plugin. :param lineage: Certificate lineage object :type lineage: storage.RenewableCert :param installer: Installer object :type installer: interfaces.Installer :returns: `None` :rtype: None """ if not config.disable_renew_updates: if isinstance(installer, interfaces.GenericUpdater): installer.generic_updates(lineage) def _run_enhancement_updaters(lineage: storage.RenewableCert, installer: interfaces.Installer, config: configuration.NamespaceConfig) -> None: """Iterates through known enhancement interfaces. If the installer implements an enhancement interface and the enhance interface has an updater method, the updater method gets run. :param lineage: Certificate lineage object :type lineage: storage.RenewableCert :param installer: Installer object :type installer: interfaces.Installer :param config: Configuration object :type config: certbot.configuration.NamespaceConfig """ if config.disable_renew_updates: return for enh in enhancements._INDEX: # pylint: disable=protected-access if isinstance(installer, enh["class"]) and enh["updater_function"]: getattr(installer, enh["updater_function"])(lineage) def _run_enhancement_deployers(lineage: storage.RenewableCert, installer: interfaces.Installer, config: configuration.NamespaceConfig) -> None: """Iterates through known enhancement interfaces. If the installer implements an enhancement interface and the enhance interface has an deployer method, the deployer method gets run. :param lineage: Certificate lineage object :type lineage: storage.RenewableCert :param installer: Installer object :type installer: interfaces.Installer :param config: Configuration object :type config: certbot.configuration.NamespaceConfig """ if config.disable_renew_updates: return for enh in enhancements._INDEX: # pylint: disable=protected-access if isinstance(installer, enh["class"]) and enh["deployer_function"]: getattr(installer, enh["deployer_function"])(lineage) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/achallenges.py0000664000175100017510000000375014561227515016457 0ustar00ericaerica"""Client annotated ACME challenges. Please use names such as ``achall`` to distinguish from variables "of type" :class:`acme.challenges.Challenge` (denoted by ``chall``) and :class:`.ChallengeBody` (denoted by ``challb``):: from acme import challenges from acme import messages from certbot import achallenges chall = challenges.DNS(token='foo') challb = messages.ChallengeBody(chall=chall) achall = achallenges.DNS(chall=challb, domain='example.com') Note, that all annotated challenges act as a proxy objects:: achall.token == challb.token """ import logging from typing import Any from typing import Type import josepy as jose from acme import challenges from acme.challenges import Challenge logger = logging.getLogger(__name__) class AnnotatedChallenge(jose.ImmutableMap): """Client annotated challenge. Wraps around server provided challenge and annotates with data useful for the client. :ivar ~.challb: Wrapped `~.ChallengeBody`. """ __slots__ = ('challb',) _acme_type: Type[Challenge] = NotImplemented def __getattr__(self, name: str) -> Any: return getattr(self.challb, name) class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): """Client annotated `KeyAuthorizationChallenge` challenge.""" __slots__ = ('challb', 'domain', 'account_key') # pylint: disable=redefined-slots-in-subclass def response_and_validation(self, *args: Any, **kwargs: Any) -> Any: """Generate response and validation.""" return self.challb.chall.response_and_validation( self.account_key, *args, **kwargs) class DNS(AnnotatedChallenge): """Client annotated "dns" ACME challenge.""" __slots__ = ('challb', 'domain') # pylint: disable=redefined-slots-in-subclass acme_type = challenges.DNS class Other(AnnotatedChallenge): """Client annotated ACME challenge of an unknown type.""" __slots__ = ('challb', 'domain') # pylint: disable=redefined-slots-in-subclass acme_type = challenges.Challenge ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3350835 certbot-2.9.0/certbot/compat/0000775000175100017510000000000014561227516015116 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/compat/__init__.py0000664000175100017510000000037114561227515017227 0ustar00ericaerica""" Compatibility layer to run certbot both on Linux and Windows. This package contains all logic that needs to be implemented specifically for Linux and for Windows. Then the rest of certbot code relies on this module to be platform agnostic. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/compat/_path.py0000664000175100017510000000340514561227515016564 0ustar00ericaerica""" This compat module wraps os.path to forbid some functions. isort:skip_file """ # NB: Each function defined in compat._path is marked with "type: ignore" to avoid mypy # to complain that a function is redefined (because we imported if first from os.path). # pylint: disable=function-redefined from __future__ import absolute_import # First round of wrapping: we import statically all public attributes exposed by the os.path # module. This allows in particular to have pylint, mypy, IDEs be aware that most of os.path # members are available in certbot.compat.path. from os.path import * # pylint: disable=wildcard-import,unused-wildcard-import,os-module-forbidden # Second round of wrapping: we import dynamically all attributes from the os.path module that have # not yet been imported by the first round (static star import). import os.path as std_os_path # pylint: disable=os-module-forbidden import sys as std_sys ourselves = std_sys.modules[__name__] for attribute in dir(std_os_path): # Check if the attribute does not already exist in our module. It could be internal attributes # of the module (__name__, __doc__), or attributes from standard os.path already imported with # `from os.path import *`. if not hasattr(ourselves, attribute): setattr(ourselves, attribute, getattr(std_os_path, attribute)) # Clean all remaining importables that are not from the core os.path module. del ourselves, std_os_path, std_sys # Function os.path.realpath is broken on some versions of Python for Windows. def realpath(*unused_args, **unused_kwargs): # type: ignore """Method os.path.realpath() is forbidden""" raise RuntimeError('Usage of os.path.realpath() is forbidden. ' 'Use certbot.compat.filesystem.realpath() instead.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/compat/filesystem.py0000664000175100017510000007340614561227515017665 0ustar00ericaerica"""Compat module to handle files security on Windows and Linux""" from __future__ import absolute_import from contextlib import contextmanager import errno import os # pylint: disable=os-module-forbidden import stat import sys from typing import Any from typing import Dict from typing import Generator from typing import List from typing import Optional try: import ntsecuritycon import pywintypes import win32api import win32con import win32file import win32security import winerror except ImportError: POSIX_MODE = True else: POSIX_MODE = False # Windows umask implementation, since Windows does not have a concept of umask by default. # We choose 022 as initial value since it is the default one on most Linux distributions, and # it is a decent choice to not have write permissions for group owner and everybody by default. # We use a class here to avoid needing to define a global variable, and the potential mistakes # that could happen with this kind of pattern. class _WindowsUmask: """Store the current umask to apply on Windows""" def __init__(self) -> None: self.mask = 0o022 _WINDOWS_UMASK = _WindowsUmask() def chmod(file_path: str, mode: int) -> None: """ Apply a POSIX mode on given file_path: - for Linux, the POSIX mode will be directly applied using chmod, - for Windows, the POSIX mode will be translated into a Windows DACL that make sense for Certbot context, and applied to the file using kernel calls. The definition of the Windows DACL that correspond to a POSIX mode, in the context of Certbot, is explained at https://github.com/certbot/certbot/issues/6356 and is implemented by the method `_generate_windows_flags()`. :param str file_path: Path of the file :param int mode: POSIX mode to apply """ if POSIX_MODE: os.chmod(file_path, mode) else: _apply_win_mode(file_path, mode) def umask(mask: int) -> int: """ Set the current numeric umask and return the previous umask. On Linux, the built-in umask method is used. On Windows, our Certbot-side implementation is used. :param int mask: The user file-creation mode mask to apply. :rtype: int :return: The previous umask value. """ if POSIX_MODE: return os.umask(mask) previous_umask = _WINDOWS_UMASK.mask _WINDOWS_UMASK.mask = mask return previous_umask @contextmanager def temp_umask(mask: int) -> Generator[None, None, None]: """ Apply a umask temporarily, meant to be used in a `with` block. Uses the Certbot implementation of umask. :param int mask: The user file-creation mode mask to apply temporarily """ old_umask: Optional[int] = None try: old_umask = umask(mask) yield None finally: if old_umask is not None: umask(old_umask) # One could ask why there is no copy_ownership() function, or even a reimplementation # of os.chown() that would modify the ownership of file without touching the mode itself. # This is because on Windows, it would require recalculating the existing DACL against # the new owner, since the DACL is composed of ACEs that targets a specific user, not dynamically # the current owner of a file. This action would be necessary to keep consistency between # the POSIX mode applied to the file and the current owner of this file. # Since copying and editing arbitrary DACL is very difficult, and since we actually know # the mode to apply at the time the owner of a file should change, it is easier to just # change the owner, then reapply the known mode, as copy_ownership_and_apply_mode() does. def copy_ownership_and_apply_mode(src: str, dst: str, mode: int, copy_user: bool, copy_group: bool) -> None: """ Copy ownership (user and optionally group on Linux) from the source to the destination, then apply given mode in compatible way for Linux and Windows. This replaces the os.chown command. :param str src: Path of the source file :param str dst: Path of the destination file :param int mode: Permission mode to apply on the destination file :param bool copy_user: Copy user if `True` :param bool copy_group: Copy group if `True` on Linux (has no effect on Windows) """ if POSIX_MODE: stats = os.stat(src) user_id = stats.st_uid if copy_user else -1 group_id = stats.st_gid if copy_group else -1 # On Windows, os.chown does not exist. This is checked through POSIX_MODE value, # but MyPy/PyLint does not know it and raises an error here on Windows. # We disable specifically the check to fix the issue. os.chown(dst, user_id, group_id) elif copy_user: # There is no group handling in Windows _copy_win_ownership(src, dst) chmod(dst, mode) # Quite similar to copy_ownership_and_apply_mode, but this time the DACL is copied from # the source file on Windows. The DACL stays consistent with the dynamic rights of the # equivalent POSIX mode, because ownership and mode are copied altogether on the destination # file, so no recomputing of the DACL against the new owner is needed, as it would be # for a copy_ownership alone method. def copy_ownership_and_mode(src: str, dst: str, copy_user: bool = True, copy_group: bool = True) -> None: """ Copy ownership (user and optionally group on Linux) and mode/DACL from the source to the destination. :param str src: Path of the source file :param str dst: Path of the destination file :param bool copy_user: Copy user if `True` :param bool copy_group: Copy group if `True` on Linux (has no effect on Windows) """ if POSIX_MODE: # On Linux, we just delegate to chown and chmod. stats = os.stat(src) user_id = stats.st_uid if copy_user else -1 group_id = stats.st_gid if copy_group else -1 os.chown(dst, user_id, group_id) chmod(dst, stats.st_mode) else: if copy_user: # There is no group handling in Windows _copy_win_ownership(src, dst) _copy_win_mode(src, dst) def check_mode(file_path: str, mode: int) -> bool: """ Check if the given mode matches the permissions of the given file. On Linux, will make a direct comparison, on Windows, mode will be compared against the security model. :param str file_path: Path of the file :param int mode: POSIX mode to test :rtype: bool :return: True if the POSIX mode matches the file permissions """ if POSIX_MODE: return stat.S_IMODE(os.stat(file_path).st_mode) == mode return _check_win_mode(file_path, mode) def check_owner(file_path: str) -> bool: """ Check if given file is owned by current user. :param str file_path: File path to check :rtype: bool :return: True if given file is owned by current user, False otherwise. """ if POSIX_MODE: return os.stat(file_path).st_uid == os.getuid() # Get owner sid of the file security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION) user = security.GetSecurityDescriptorOwner() # Compare sids return _get_current_user() == user def check_permissions(file_path: str, mode: int) -> bool: """ Check if given file has the given mode and is owned by current user. :param str file_path: File path to check :param int mode: POSIX mode to check :rtype: bool :return: True if file has correct mode and owner, False otherwise. """ return check_owner(file_path) and check_mode(file_path, mode) def open(file_path: str, flags: int, mode: int = 0o777) -> int: # pylint: disable=redefined-builtin """ Wrapper of original os.open function, that will ensure on Windows that given mode is correctly applied. :param str file_path: The file path to open :param int flags: Flags to apply on file while opened :param int mode: POSIX mode to apply on file when opened, Python defaults will be applied if ``None`` :returns: the file descriptor to the opened file :rtype: int :raise: OSError(errno.EEXIST) if the file already exists and os.O_CREAT & os.O_EXCL are set, OSError(errno.EACCES) on Windows if the file already exists and is a directory, and os.O_CREAT is set. """ if POSIX_MODE: # On Linux, invoke os.open directly. return os.open(file_path, flags, mode) # Windows: handle creation of the file atomically with proper permissions. if flags & os.O_CREAT: # If os.O_EXCL is set, we will use the "CREATE_NEW", that will raise an exception if # file exists, matching the API contract of this bit flag. Otherwise, we use # "CREATE_ALWAYS" that will always create the file whether it exists or not. disposition = win32con.CREATE_NEW if flags & os.O_EXCL else win32con.CREATE_ALWAYS attributes = win32security.SECURITY_ATTRIBUTES() security = attributes.SECURITY_DESCRIPTOR user = _get_current_user() dacl = _generate_dacl(user, mode, _WINDOWS_UMASK.mask) # We set second parameter to 0 (`False`) to say that this security descriptor is # NOT constructed from a default mechanism, but is explicitly set by the user. # See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-setsecuritydescriptorowner # pylint: disable=line-too-long security.SetSecurityDescriptorOwner(user, 0) # We set first parameter to 1 (`True`) to say that this security descriptor contains # a DACL. Otherwise second and third parameters are ignored. # We set third parameter to 0 (`False`) to say that this security descriptor is # NOT constructed from a default mechanism, but is explicitly set by the user. # See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-setsecuritydescriptordacl # pylint: disable=line-too-long security.SetSecurityDescriptorDacl(1, dacl, 0) handle = None try: handle = win32file.CreateFile(file_path, win32file.GENERIC_READ, win32file.FILE_SHARE_READ & win32file.FILE_SHARE_WRITE, attributes, disposition, 0, None) except pywintypes.error as err: # Handle native windows errors into python errors to be consistent with the API # of os.open in the situation of a file already existing or locked. if err.winerror == winerror.ERROR_FILE_EXISTS: raise OSError(errno.EEXIST, err.strerror) if err.winerror == winerror.ERROR_SHARING_VIOLATION: raise OSError(errno.EACCES, err.strerror) raise err finally: if handle: handle.Close() # At this point, the file that did not exist has been created with proper permissions, # so os.O_CREAT and os.O_EXCL are not needed anymore. We remove them from the flags to # avoid a FileExists exception before calling os.open. return os.open(file_path, flags ^ os.O_CREAT ^ os.O_EXCL) # Windows: general case, we call os.open, let exceptions be thrown, then chmod if all is fine. fd = os.open(file_path, flags) chmod(file_path, mode) return fd def makedirs(file_path: str, mode: int = 0o777) -> None: """ Rewrite of original os.makedirs function, that will ensure on Windows that given mode is correctly applied. :param str file_path: The file path to open :param int mode: POSIX mode to apply on leaf directory when created, Python defaults will be applied if ``None`` """ current_umask = umask(0) try: # Since Python 3.7, os.makedirs does not set the given mode to the intermediate # directories that could be created in the process. To keep things safe and consistent # on all Python versions, we set the umask accordingly to have all directories # (intermediate and leaf) created with the given mode. umask(current_umask | 0o777 ^ mode) if POSIX_MODE: return os.makedirs(file_path, mode) orig_mkdir_fn = os.mkdir try: # As we know that os.mkdir is called internally by os.makedirs, we will swap the # function in os module for the time of makedirs execution on Windows. os.mkdir = mkdir # type: ignore return os.makedirs(file_path, mode) finally: os.mkdir = orig_mkdir_fn finally: umask(current_umask) def mkdir(file_path: str, mode: int = 0o777) -> None: """ Rewrite of original os.mkdir function, that will ensure on Windows that given mode is correctly applied. :param str file_path: The file path to open :param int mode: POSIX mode to apply on directory when created, Python defaults will be applied if ``None`` """ if POSIX_MODE: return os.mkdir(file_path, mode) attributes = win32security.SECURITY_ATTRIBUTES() security = attributes.SECURITY_DESCRIPTOR user = _get_current_user() dacl = _generate_dacl(user, mode, _WINDOWS_UMASK.mask) security.SetSecurityDescriptorOwner(user, False) security.SetSecurityDescriptorDacl(1, dacl, 0) try: win32file.CreateDirectory(file_path, attributes) except pywintypes.error as err: # Handle native windows error into python error to be consistent with the API # of os.mkdir in the situation of a directory already existing. if err.winerror == winerror.ERROR_ALREADY_EXISTS: raise OSError(errno.EEXIST, err.strerror, file_path, err.winerror) raise err return None def replace(src: str, dst: str) -> None: """ Rename a file to a destination path and handles situations where the destination exists. :param str src: The current file path. :param str dst: The new file path. """ if hasattr(os, 'replace'): # Use replace if possible. Since we don't support Python 2 on Windows # and os.replace() was added in Python 3.3, we can assume that # os.replace() is always available on Windows. getattr(os, 'replace')(src, dst) else: # Otherwise, use os.rename() that behaves like os.replace() on Linux. os.rename(src, dst) def realpath(file_path: str) -> str: """ Find the real path for the given path. This method resolves symlinks, including recursive symlinks, and is protected against symlinks that creates an infinite loop. :param str file_path: The path to resolve :returns: The real path for the given path :rtype: str """ original_path = file_path # Since Python 3.8, os.path.realpath also resolves symlinks on Windows. if POSIX_MODE or sys.version_info >= (3, 8): path = os.path.realpath(file_path) if os.path.islink(path): # If path returned by realpath is still a link, it means that it failed to # resolve the symlink because of a loop. # See realpath code: https://github.com/python/cpython/blob/master/Lib/posixpath.py raise RuntimeError('Error, link {0} is a loop!'.format(original_path)) return path inspected_paths: List[str] = [] while os.path.islink(file_path): link_path = file_path file_path = os.readlink(file_path) if not os.path.isabs(file_path): file_path = os.path.join(os.path.dirname(link_path), file_path) if file_path in inspected_paths: raise RuntimeError('Error, link {0} is a loop!'.format(original_path)) inspected_paths.append(file_path) return os.path.abspath(file_path) def readlink(link_path: str) -> str: """ Return a string representing the path to which the symbolic link points. :param str link_path: The symlink path to resolve :return: The path the symlink points to :returns: str :raise: ValueError if a long path (260> characters) is encountered on Windows """ path = os.readlink(link_path) if POSIX_MODE or not path.startswith('\\\\?\\'): return path # At this point, we know we are on Windows and that the path returned uses # the extended form which is done for all paths in Python 3.8+ # Max length of a normal path is 260 characters on Windows, including the non printable # termination character "". The termination character is not included in Python # strings, giving a max length of 259 characters, + 4 characters for the extended form # prefix, to an effective max length 263 characters on a string representing a normal path. if len(path) < 264: return path[4:] raise ValueError("Long paths are not supported by Certbot on Windows.") # On Windows is_executable run from an unprivileged shell may claim that a path is # executable when it is executable only if run from a privileged shell. This result # is due to the fact that GetEffectiveRightsFromAcl calculate effective rights # without taking into consideration if the target user has currently required the # elevated privileges or not. However this is not a problem since certbot always # requires to be run under a privileged shell, so the user will always benefit # from the highest (privileged one) set of permissions on a given file. def is_executable(path: str) -> bool: """ Is path an executable file? :param str path: path to test :return: True if path is an executable file :rtype: bool """ if POSIX_MODE: return os.path.isfile(path) and os.access(path, os.X_OK) return _win_is_executable(path) def has_world_permissions(path: str) -> bool: """ Check if everybody/world has any right (read/write/execute) on a file given its path. :param str path: path to test :return: True if everybody/world has any right to the file :rtype: bool """ if POSIX_MODE: return bool(stat.S_IMODE(os.stat(path).st_mode) & stat.S_IRWXO) security = win32security.GetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION) dacl = security.GetSecurityDescriptorDacl() return bool(dacl.GetEffectiveRightsFromAcl({ 'TrusteeForm': win32security.TRUSTEE_IS_SID, 'TrusteeType': win32security.TRUSTEE_IS_USER, 'Identifier': win32security.ConvertStringSidToSid('S-1-1-0'), })) def compute_private_key_mode(old_key: str, base_mode: int) -> int: """ Calculate the POSIX mode to apply to a private key given the previous private key. :param str old_key: path to the previous private key :param int base_mode: the minimum modes to apply to a private key :return: the POSIX mode to apply :rtype: int """ if POSIX_MODE: # On Linux, we keep read/write/execute permissions # for group and read permissions for everybody. old_mode = (stat.S_IMODE(os.stat(old_key).st_mode) & (stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH)) return base_mode | old_mode # On Windows, the mode returned by os.stat is not reliable, # so we do not keep any permission from the previous private key. return base_mode def has_same_ownership(path1: str, path2: str) -> bool: """ Return True if the ownership of two files given their respective path is the same. On Windows, ownership is checked against owner only, since files do not have a group owner. :param str path1: path to the first file :param str path2: path to the second file :return: True if both files have the same ownership, False otherwise :rtype: bool """ if POSIX_MODE: stats1 = os.stat(path1) stats2 = os.stat(path2) return (stats1.st_uid, stats1.st_gid) == (stats2.st_uid, stats2.st_gid) security1 = win32security.GetFileSecurity(path1, win32security.OWNER_SECURITY_INFORMATION) user1 = security1.GetSecurityDescriptorOwner() security2 = win32security.GetFileSecurity(path2, win32security.OWNER_SECURITY_INFORMATION) user2 = security2.GetSecurityDescriptorOwner() return user1 == user2 def has_min_permissions(path: str, min_mode: int) -> bool: """ Check if a file given its path has at least the permissions defined by the given minimal mode. On Windows, group permissions are ignored since files do not have a group owner. :param str path: path to the file to check :param int min_mode: the minimal permissions expected :return: True if the file matches the minimal permissions expectations, False otherwise :rtype: bool """ if POSIX_MODE: st_mode = os.stat(path).st_mode return st_mode == st_mode | min_mode # Resolve symlinks, to get a consistent result with os.stat on Linux, # that follows symlinks by default. path = realpath(path) # Get owner sid of the file security = win32security.GetFileSecurity( path, win32security.OWNER_SECURITY_INFORMATION | win32security.DACL_SECURITY_INFORMATION) user = security.GetSecurityDescriptorOwner() dacl = security.GetSecurityDescriptorDacl() min_dacl = _generate_dacl(user, min_mode) for index in range(min_dacl.GetAceCount()): min_ace = min_dacl.GetAce(index) # On a given ACE, index 0 is the ACE type, 1 is the permission mask, and 2 is the SID. # See: http://timgolden.me.uk/pywin32-docs/PyACL__GetAce_meth.html mask = min_ace[1] user = min_ace[2] effective_mask = dacl.GetEffectiveRightsFromAcl({ 'TrusteeForm': win32security.TRUSTEE_IS_SID, 'TrusteeType': win32security.TRUSTEE_IS_USER, 'Identifier': user, }) if effective_mask != effective_mask | mask: return False return True def _win_is_executable(path: str) -> bool: if not os.path.isfile(path): return False security = win32security.GetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION) dacl = security.GetSecurityDescriptorDacl() mode = dacl.GetEffectiveRightsFromAcl({ 'TrusteeForm': win32security.TRUSTEE_IS_SID, 'TrusteeType': win32security.TRUSTEE_IS_USER, 'Identifier': _get_current_user(), }) return mode & ntsecuritycon.FILE_GENERIC_EXECUTE == ntsecuritycon.FILE_GENERIC_EXECUTE def _apply_win_mode(file_path: str, mode: int) -> None: """ This function converts the given POSIX mode into a Windows ACL list, and applies it to the file given its path. If the given path is a symbolic link, it will resolved to apply the mode on the targeted file. """ file_path = realpath(file_path) # Get owner sid of the file security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION) user = security.GetSecurityDescriptorOwner() # New DACL, that will overwrite existing one (including inherited permissions) dacl = _generate_dacl(user, mode) # Apply the new DACL security.SetSecurityDescriptorDacl(1, dacl, 0) win32security.SetFileSecurity(file_path, win32security.DACL_SECURITY_INFORMATION, security) def _generate_dacl(user_sid: Any, mode: int, mask: Optional[int] = None) -> Any: if mask: mode = mode & (0o777 - mask) analysis = _analyze_mode(mode) # Get standard accounts from "well-known" sid # See the list here: # https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems system = win32security.ConvertStringSidToSid('S-1-5-18') admins = win32security.ConvertStringSidToSid('S-1-5-32-544') everyone = win32security.ConvertStringSidToSid('S-1-1-0') # New dacl, without inherited permissions dacl = win32security.ACL() # If user is already system or admins, any ACE defined here would be superseded by # the full control ACE that will be added after. if user_sid not in [system, admins]: # Handle user rights user_flags = _generate_windows_flags(analysis['user']) if user_flags: dacl.AddAccessAllowedAce(win32security.ACL_REVISION, user_flags, user_sid) # Handle everybody rights everybody_flags = _generate_windows_flags(analysis['all']) if everybody_flags: dacl.AddAccessAllowedAce(win32security.ACL_REVISION, everybody_flags, everyone) # Handle administrator rights full_permissions = _generate_windows_flags({'read': True, 'write': True, 'execute': True}) dacl.AddAccessAllowedAce(win32security.ACL_REVISION, full_permissions, system) dacl.AddAccessAllowedAce(win32security.ACL_REVISION, full_permissions, admins) return dacl def _analyze_mode(mode: int) -> Dict[str, Dict[str, int]]: return { 'user': { 'read': mode & stat.S_IRUSR, 'write': mode & stat.S_IWUSR, 'execute': mode & stat.S_IXUSR, }, 'all': { 'read': mode & stat.S_IROTH, 'write': mode & stat.S_IWOTH, 'execute': mode & stat.S_IXOTH, }, } def _copy_win_ownership(src: str, dst: str) -> None: # Resolve symbolic links src = realpath(src) security_src = win32security.GetFileSecurity(src, win32security.OWNER_SECURITY_INFORMATION) user_src = security_src.GetSecurityDescriptorOwner() security_dst = win32security.GetFileSecurity(dst, win32security.OWNER_SECURITY_INFORMATION) # Second parameter indicates, if `False`, that the owner of the file is not provided by some # default mechanism, but is explicitly set instead. This is obviously what we are doing here. security_dst.SetSecurityDescriptorOwner(user_src, False) win32security.SetFileSecurity(dst, win32security.OWNER_SECURITY_INFORMATION, security_dst) def _copy_win_mode(src: str, dst: str) -> None: # Resolve symbolic links src = realpath(src) # Copy the DACL from src to dst. security_src = win32security.GetFileSecurity(src, win32security.DACL_SECURITY_INFORMATION) dacl = security_src.GetSecurityDescriptorDacl() security_dst = win32security.GetFileSecurity(dst, win32security.DACL_SECURITY_INFORMATION) security_dst.SetSecurityDescriptorDacl(1, dacl, 0) win32security.SetFileSecurity(dst, win32security.DACL_SECURITY_INFORMATION, security_dst) def _generate_windows_flags(rights_desc: Dict[str, int]) -> int: # Some notes about how each POSIX right is interpreted. # # For the rights read and execute, we have a pretty bijective relation between # POSIX flags and their generic counterparts on Windows, so we use them directly # (respectively ntsecuritycon.FILE_GENERIC_READ and ntsecuritycon.FILE_GENERIC_EXECUTE). # # But ntsecuritycon.FILE_GENERIC_WRITE does not correspond to what one could expect from a # write access on Linux: for Windows, FILE_GENERIC_WRITE does not include delete, move or # rename. This is something that requires ntsecuritycon.FILE_ALL_ACCESS. # So to reproduce the write right as POSIX, we will apply ntsecuritycon.FILE_ALL_ACCESS # subtracted of the rights corresponding to POSIX read and POSIX execute. # # Finally, having read + write + execute gives a ntsecuritycon.FILE_ALL_ACCESS, # so a "Full Control" on the file. # # A complete list of the rights defined on NTFS can be found here: # https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783530(v=ws.10)#permissions-for-files-and-folders flag = 0 if rights_desc['read']: flag = flag | ntsecuritycon.FILE_GENERIC_READ if rights_desc['write']: flag = flag | (ntsecuritycon.FILE_ALL_ACCESS ^ ntsecuritycon.FILE_GENERIC_READ ^ ntsecuritycon.FILE_GENERIC_EXECUTE) if rights_desc['execute']: flag = flag | ntsecuritycon.FILE_GENERIC_EXECUTE return flag def _check_win_mode(file_path: str, mode: int) -> bool: # Resolve symbolic links file_path = realpath(file_path) # Get current dacl file security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION | win32security.DACL_SECURITY_INFORMATION) dacl = security.GetSecurityDescriptorDacl() # Get current file owner sid user = security.GetSecurityDescriptorOwner() if not dacl: # No DACL means full control to everyone # This is not a deterministic permissions set. return False # Calculate the target dacl ref_dacl = _generate_dacl(user, mode) return _compare_dacls(dacl, ref_dacl) def _compare_dacls(dacl1: Any, dacl2: Any) -> bool: """ This method compare the two given DACLs to check if they are identical. Identical means here that they contains the same set of ACEs in the same order. """ return ([dacl1.GetAce(index) for index in range(dacl1.GetAceCount())] == [dacl2.GetAce(index) for index in range(dacl2.GetAceCount())]) def _get_current_user() -> Any: """ Return the pySID corresponding to the current user. """ # We craft the account_name ourselves instead of calling for instance win32api.GetUserNameEx, # because this function returns nonsense values when Certbot is run under NT AUTHORITY\SYSTEM. # To run Certbot under NT AUTHORITY\SYSTEM, you can open a shell using the instructions here: # https://blogs.technet.microsoft.com/ben_parker/2010/10/27/how-do-i-run-powershell-execommand-prompt-as-the-localsystem-account-on-windows-7/ account_name = r"{0}\{1}".format(win32api.GetDomainName(), win32api.GetUserName()) # LookupAccountName() expects the system name as first parameter. By passing None to it, # we instruct Windows to first search the matching account in the machine local accounts, # then into the primary domain accounts, if the machine has joined a domain, then finally # into the trusted domains accounts. This is the preferred lookup mechanism to use in Windows # if there is no reason to use a specific lookup mechanism. # See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-lookupaccountnamea return win32security.LookupAccountName(None, account_name)[0] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/compat/misc.py0000664000175100017510000001244414561227515016427 0ustar00ericaerica""" This compat module handles various platform specific calls that do not fall into one particular category. """ from __future__ import absolute_import import logging import select import subprocess import sys from typing import Optional from typing import Tuple from certbot import errors from certbot.compat import os try: from pywintypes import error as pywinerror from win32com.shell import shell as shellwin32 from win32console import GetStdHandle from win32console import STD_OUTPUT_HANDLE POSIX_MODE = False except ImportError: # pragma: no cover POSIX_MODE = True logger = logging.getLogger(__name__) # For Linux: define OS specific standard binary directories STANDARD_BINARY_DIRS = ["/usr/sbin", "/usr/local/bin", "/usr/local/sbin"] if POSIX_MODE else [] def raise_for_non_administrative_windows_rights() -> None: """ On Windows, raise if current shell does not have the administrative rights. Do nothing on Linux. :raises .errors.Error: If the current shell does not have administrative rights on Windows. """ if not POSIX_MODE and shellwin32.IsUserAnAdmin() == 0: # pragma: no cover raise errors.Error('Error, certbot must be run on a shell with administrative rights.') def prepare_virtual_console() -> None: """ On Windows, ensure that Console Virtual Terminal Sequences are enabled. """ if POSIX_MODE: return # https://docs.microsoft.com/en-us/windows/console/setconsolemode ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 # stdout/stderr will be the same console screen buffer, but this could return None or raise try: h = GetStdHandle(STD_OUTPUT_HANDLE) if h: h.SetConsoleMode(h.GetConsoleMode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING) except pywinerror: logger.debug("Failed to set console mode", exc_info=True) def readline_with_timeout(timeout: float, prompt: Optional[str]) -> str: """ Read user input to return the first line entered, or raise after specified timeout. :param float timeout: The timeout in seconds given to the user. :param str prompt: The prompt message to display to the user. :returns: The first line entered by the user. :rtype: str """ try: # Linux specific # # Call to select can only be done like this on UNIX rlist, _, _ = select.select([sys.stdin], [], [], timeout) if not rlist: raise errors.Error( "Timed out waiting for answer to prompt '{0}'".format(prompt if prompt else "")) return rlist[0].readline() except OSError: # Windows specific # # No way with select to make a timeout to the user input on Windows, # as select only supports socket in this case. # So no timeout on Windows for now. return sys.stdin.readline() WINDOWS_DEFAULT_FOLDERS = { 'config': 'C:\\Certbot', 'work': 'C:\\Certbot\\lib', 'logs': 'C:\\Certbot\\log', } LINUX_DEFAULT_FOLDERS = { 'config': '/etc/letsencrypt', 'work': '/var/lib/letsencrypt', 'logs': '/var/log/letsencrypt', } def get_default_folder(folder_type: str) -> str: """ Return the relevant default folder for the current OS :param str folder_type: The type of folder to retrieve (config, work or logs) :returns: The relevant default folder. :rtype: str """ if os.name != 'nt': # Linux specific return LINUX_DEFAULT_FOLDERS[folder_type] # Windows specific return WINDOWS_DEFAULT_FOLDERS[folder_type] def underscores_for_unsupported_characters_in_path(path: str) -> str: """ Replace unsupported characters in path for current OS by underscores. :param str path: the path to normalize :return: the normalized path :rtype: str """ if os.name != 'nt': # Linux specific return path # Windows specific drive, tail = os.path.splitdrive(path) return drive + tail.replace(':', '_') def execute_command_status(cmd_name: str, shell_cmd: str, env: Optional[dict] = None) -> Tuple[int, str, str]: """ Run a command: - on Linux command will be run by the standard shell selected with subprocess.run(shell=True) - on Windows command will be run in a Powershell shell This function returns the exit code, and does not log the result and output of the command. :param str cmd_name: the user facing name of the hook being run :param str shell_cmd: shell command to execute :param dict env: environ to pass into subprocess.run :returns: `tuple` (`int` returncode, `str` stderr, `str` stdout) """ logger.info("Running %s command: %s", cmd_name, shell_cmd) if POSIX_MODE: proc = subprocess.run(shell_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=False, env=env) else: line = ['powershell.exe', '-Command', shell_cmd] proc = subprocess.run(line, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=False, env=env) # universal_newlines causes stdout and stderr to be str objects instead of # bytes in Python 3 out, err = proc.stdout, proc.stderr return proc.returncode, err, out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/compat/os.py0000664000175100017510000002244714561227515016121 0ustar00ericaerica""" This compat modules is a wrapper of the core os module that forbids usage of specific operations (e.g. chown, chmod, getuid) that would be harmful to the Windows file security model of Certbot. This module is intended to replace standard os module throughout certbot projects (except acme). This module has the same API as the os module in the Python standard library except for the functions defined below. isort:skip_file """ # NB1: If adding a new documented function to compat.os, ensure that it is added to the # ':members:' list in certbot/docs/api/certbot.compat.os.rst. # NB2: Each function defined in compat.os is marked with "type: ignore" to avoid mypy # to complain that a function is redefined (because we imported if first from os). # pylint: disable=function-redefined from __future__ import absolute_import # First round of wrapping: we import statically all public attributes exposed by the os module # This allows in particular to have pylint, mypy, IDEs be aware that most of os members are # available in certbot.compat.os. from os import * # pylint: disable=wildcard-import,unused-wildcard-import,redefined-builtin,os-module-forbidden # Second round of wrapping: we import dynamically all attributes from the os module that have not # yet been imported by the first round (static import). This covers in particular the case of # specific python 3.x versions where not all public attributes are in the special __all__ of os, # and so not in `from os import *`. import os as std_os # pylint: disable=os-module-forbidden import sys as std_sys ourselves = std_sys.modules[__name__] # Adding all of stdlib os to this module confuses Sphinx so we skip this when # building the documentation. if not std_os.environ.get("CERTBOT_DOCS") == "1": for attribute in dir(std_os): # Check if the attribute does not already exist in our module. It could # be internal attributes of the module (__name__, __doc__), or # attributes from standard os already imported with `from os import *`. if not hasattr(ourselves, attribute): setattr(ourselves, attribute, getattr(std_os, attribute)) # Import our internal path module, then allow certbot.compat.os.path # to behave as a module (similarly to os.path). from certbot.compat import _path as path # type: ignore # pylint: disable=wrong-import-position std_sys.modules[__name__ + '.path'] = path # Clean all remaining importables that are not from the core os module. del ourselves, std_os, std_sys # Chmod is the root of all evil for our security model on Windows. With the default implementation # of os.chmod on Windows, almost all bits on mode will be ignored, and only a general RO or RW will # be applied. The DACL, the inner mechanism to control file access on Windows, will stay on its # default definition, giving effectively at least read permissions to any one, as the default # permissions on root path will be inherit by the file (as NTFS state), and root path can be read # by anyone. So the given mode needs to be translated into a secured and not inherited DACL that # will be applied to this file using filesystem.chmod, calling internally the win32security # module to construct and apply the DACL. Complete security model to translate a POSIX mode into # a suitable DACL on Windows for Certbot can be found here: # https://github.com/certbot/certbot/issues/6356 # Basically, it states that appropriate permissions will be set for the owner, nothing for the # group, appropriate permissions for the "Everyone" group, and all permissions to the # "Administrators" group + "System" user, as they can do everything anyway. def chmod(*unused_args, **unused_kwargs): # type: ignore """Method os.chmod() is forbidden""" raise RuntimeError('Usage of os.chmod() is forbidden. ' 'Use certbot.compat.filesystem.chmod() instead.') # Since there is no mode on Windows, there is no umask either, and so this method is a noop for # this platform. In order to have a consistent behavior between Linux and Windows on Certbot files # and directories, the filesystem umask method must be used instead, since it implements umask for # Windows. def umask(*unused_args, **unused_kwargs): # type: ignore """Method os.chmod() is forbidden""" raise RuntimeError('Usage of os.umask() is forbidden. ' 'Use certbot.compat.filesystem.umask() instead.') # Because uid is not a concept on Windows, chown is useless. In fact, it is not even available # on Python for Windows. So to be consistent on both platforms for Certbot, this method is # always forbidden. def chown(*unused_args, **unused_kwargs): # type: ignore """Method os.chown() is forbidden""" raise RuntimeError('Usage of os.chown() is forbidden.' 'Use certbot.compat.filesystem.copy_ownership_and_apply_mode() instead.') # The os.open function on Windows has the same effect as a call to os.chown concerning the file # modes: these modes lack the correct control over the permissions given to the file. Instead, # filesystem.open invokes the Windows native API `CreateFile` to ensure that permissions are # atomically set in case of file creation, or invokes filesystem.chmod to properly set the # permissions for the other cases. def open(*unused_args, **unused_kwargs): # type: ignore """Method os.open() is forbidden""" raise RuntimeError('Usage of os.open() is forbidden. ' 'Use certbot.compat.filesystem.open() instead.') # Very similarly to os.open, os.mkdir has the same effects on Windows and creates an unsecured # folder. So a similar mitigation to security.chmod is provided on this platform. def mkdir(*unused_args, **unused_kwargs): # type: ignore """Method os.mkdir() is forbidden""" raise RuntimeError('Usage of os.mkdir() is forbidden. ' 'Use certbot.compat.filesystem.mkdir() instead.') # As said above, os.makedirs would call the original os.mkdir function recursively on Windows, # creating the same flaws for every actual folder created. This method is modified to ensure # that our modified os.mkdir is called on Windows, by monkey patching temporarily the mkdir method # on the original os module, executing the modified logic to correctly protect newly created # folders, then restoring original mkdir method in the os module. def makedirs(*unused_args, **unused_kwargs): # type: ignore """Method os.makedirs() is forbidden""" raise RuntimeError('Usage of os.makedirs() is forbidden. ' 'Use certbot.compat.filesystem.makedirs() instead.') # Because of the blocking strategy on file handlers on Windows, rename does not behave as expected # with POSIX systems: an exception will be raised if dst already exists. def rename(*unused_args, **unused_kwargs): # type: ignore """Method os.rename() is forbidden""" raise RuntimeError('Usage of os.rename() is forbidden. ' 'Use certbot.compat.filesystem.replace() instead.') # Behavior of os.replace is consistent between Windows and Linux. However, it is not supported on # Python 2.x. So, as for os.rename, we forbid it in favor of filesystem.replace. def replace(*unused_args, **unused_kwargs): # type: ignore """Method os.replace() is forbidden""" raise RuntimeError('Usage of os.replace() is forbidden. ' 'Use certbot.compat.filesystem.replace() instead.') # Results given by os.access are inconsistent or partial on Windows, because this platform is not # following the POSIX approach. def access(*unused_args, **unused_kwargs): # type: ignore """Method os.access() is forbidden""" raise RuntimeError('Usage of os.access() is forbidden. ' 'Use certbot.compat.filesystem.check_mode() or ' 'certbot.compat.filesystem.is_executable() instead.') # On Windows os.stat call result is inconsistent, with a lot of flags that are not set or # meaningless. We need to use specialized functions from the certbot.compat.filesystem module. def stat(*unused_args, **unused_kwargs): # type: ignore """Method os.stat() is forbidden""" raise RuntimeError('Usage of os.stat() is forbidden. ' 'Use certbot.compat.filesystem functions instead ' '(eg. has_min_permissions, has_same_ownership).') # Method os.fstat has the same problem than os.stat, since it is the same function, # but accepting a file descriptor instead of a path. def fstat(*unused_args, **unused_kwargs): # type: ignore """Method os.stat() is forbidden""" raise RuntimeError('Usage of os.fstat() is forbidden. ' 'Use certbot.compat.filesystem functions instead ' '(eg. has_min_permissions, has_same_ownership).') # Method os.readlink has a significant behavior change with Python 3.8+. Starting # with this version, it will return the resolved path in its "extended-style" form # unconditionally, which allows to use more than 259 characters, and its string # representation is prepended with "\\?\". Problem is that it does it for any path, # and will make equality comparison fail with paths that will use the simple form. def readlink(*unused_args, **unused_kwargs): # type: ignore """Method os.readlink() is forbidden""" raise RuntimeError('Usage of os.readlink() is forbidden. ' 'Use certbot.compat.filesystem.realpath() instead.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/configuration.py0000664000175100017510000004525214561227515017063 0ustar00ericaerica"""Certbot user-supplied configuration.""" import argparse import copy import enum import logging from typing import Any from typing import Dict from typing import List from typing import Optional from urllib import parse import warnings from certbot import errors from certbot import util from certbot._internal import constants from certbot.compat import misc from certbot.compat import os logger = logging.getLogger(__name__) class ArgumentSource(enum.Enum): """Enum for describing where a configuration argument was set.""" COMMAND_LINE = enum.auto() """Argument was specified on the command line""" CONFIG_FILE = enum.auto() """Argument was specified in a .ini config file""" DEFAULT = enum.auto() """Argument was not set by the user, and was assigned its default value""" ENV_VAR = enum.auto() """Argument was specified in an environment variable""" RUNTIME = enum.auto() """Argument was set at runtime by certbot""" class NamespaceConfig: """Configuration wrapper around :class:`argparse.Namespace`. Please note that the following attributes are dynamically resolved using :attr:`~certbot.configuration.NamespaceConfig.work_dir` and relative paths defined in :py:mod:`certbot._internal.constants`: - `accounts_dir` - `csr_dir` - `in_progress_dir` - `key_dir` - `temp_checkpoint_dir` And the following paths are dynamically resolved using :attr:`~certbot.configuration.NamespaceConfig.config_dir` and relative paths defined in :py:mod:`certbot._internal.constants`: - `default_archive_dir` - `live_dir` - `renewal_configs_dir` :ivar namespace: Namespace typically produced by :meth:`argparse.ArgumentParser.parse_args`. :type namespace: :class:`argparse.Namespace` """ def __init__(self, namespace: argparse.Namespace) -> None: self.namespace: argparse.Namespace # Avoid recursion loop because of the delegation defined in __setattr__ object.__setattr__(self, 'namespace', namespace) object.__setattr__(self, '_argument_sources', None) object.__setattr__(self, '_previously_accessed_mutables', {}) self.namespace.config_dir = os.path.abspath(self.namespace.config_dir) self.namespace.work_dir = os.path.abspath(self.namespace.work_dir) self.namespace.logs_dir = os.path.abspath(self.namespace.logs_dir) # Check command line parameters sanity, and error out in case of problem. _check_config_sanity(self) def set_argument_sources(self, argument_sources: Dict[str, ArgumentSource]) -> None: """ Associate the NamespaceConfig with a dictionary describing where each of its arguments came from, e.g. `{ 'email': ArgumentSource.CONFIG_FILE }`. This is necessary for making runtime evaluations on whether an argument was specified by the user or not (see `set_by_user`). For an example of how to build such a dictionary, see `certbot._internal.cli.helpful.HelpfulArgumentParser._build_sources_dict` :ivar argument_sources: dictionary of argument names to their :class:`ArgumentSource` :type argument_sources: :class:`Dict[str, ArgumentSource]` """ # Avoid recursion loop because of the delegation defined in __setattr__ object.__setattr__(self, '_argument_sources', argument_sources) def set_by_user(self, var: str) -> bool: """ Return True if a particular config variable has been set by the user (via CLI or config file) including if the user explicitly set it to the default, or if it was dynamically set at runtime. Returns False if the variable was assigned a default value. Raises an exception if `argument_sources` is not set. """ from certbot._internal.cli.cli_constants import DEPRECATED_OPTIONS from certbot._internal.cli.cli_constants import VAR_MODIFIERS from certbot._internal.plugins import selection if self.argument_sources is None: raise RuntimeError( "NamespaceConfig.set_by_user called without an ArgumentSources dict. " "See NamespaceConfig.set_argument_sources().") # We should probably never actually hit this code. But if we do, # a deprecated option has logically never been set by the CLI. if var in DEPRECATED_OPTIONS: return False if var in ['authenticator', 'installer']: auth, inst = selection.cli_plugin_requests(self) if var == 'authenticator': return auth is not None if var == 'installer': return inst is not None if var in self.argument_sources and self.argument_sources[var] != ArgumentSource.DEFAULT: logger.debug("Var %s=%s (set by user).", var, getattr(self, var)) return True for modifier in VAR_MODIFIERS.get(var, []): if self.set_by_user(modifier): logger.debug("Var %s=%s (set by user).", var, VAR_MODIFIERS.get(var, [])) return True return False def to_dict(self) -> Dict[str, Any]: """ Returns a dictionary mapping all argument names to their values """ return vars(self.namespace) def _mark_runtime_override(self, name: str) -> None: """ If an argument_sources dict was set, overwrites an argument's source to be ArgumentSource.RUNTIME. Used when certbot sets an argument's values at runtime. This also clears the modified value from _previously_accessed_mutables since it is no longer needed. """ if self._argument_sources is not None: self._argument_sources[name] = ArgumentSource.RUNTIME if name in self._previously_accessed_mutables: del self._previously_accessed_mutables[name] @property def argument_sources(self) -> Optional[Dict[str, ArgumentSource]]: """Returns _argument_sources after handling any changes to accessed mutable values.""" # We keep values in _previously_accessed_mutables until we've detected a modification to try # to provide up-to-date information when argument_sources is accessed. Once a mutable object # has been accessed, it can be modified at any time if a reference to it was kept somewhere # else. # We copy _previously_accessed_mutables because _mark_runtime_override modifies it. for name, prev_value in self._previously_accessed_mutables.copy().items(): current_value = getattr(self.namespace, name) if current_value != prev_value: self._mark_runtime_override(name) return self._argument_sources # Delegate any attribute not explicitly defined to the underlying namespace object. # # If any mutable namespace attributes are explicitly defined in the future, you'll probably want # to take an approach like the one used in __getattr__ and the argument_sources property. def __getattr__(self, name: str) -> Any: arg_sources = self.argument_sources value = getattr(self.namespace, name) if arg_sources is not None: # If the requested attribute was already modified at runtime, we don't need to track any # future changes. if name not in arg_sources or arg_sources[name] != ArgumentSource.RUNTIME: # If name is already in _previously_accessed_mutables, we don't need to make a copy # of it again. If its value was changed, this would have been caught while preparing # the return value of the property self.argument_sources accessed earlier in this # function. if name not in self._previously_accessed_mutables and not _is_immutable(value): self._previously_accessed_mutables[name] = copy.deepcopy(value) return value def __setattr__(self, name: str, value: Any) -> None: self._mark_runtime_override(name) setattr(self.namespace, name, value) @property def server(self) -> str: """ACME Directory Resource URI.""" return self.namespace.server @server.setter def server(self, server_: str) -> None: self._mark_runtime_override('server') self.namespace.server = server_ @property def email(self) -> Optional[str]: """Email used for registration and recovery contact. Use comma to register multiple emails, ex: u1@example.com,u2@example.com. (default: Ask). """ return self.namespace.email @email.setter def email(self, mail: str) -> None: self._mark_runtime_override('email') self.namespace.email = mail @property def rsa_key_size(self) -> int: """Size of the RSA key.""" return self.namespace.rsa_key_size @rsa_key_size.setter def rsa_key_size(self, ksize: int) -> None: """Set the rsa_key_size property""" self._mark_runtime_override('rsa_key_size') self.namespace.rsa_key_size = ksize @property def elliptic_curve(self) -> str: """The SECG elliptic curve name to use. Please see RFC 8446 for supported values. """ return self.namespace.elliptic_curve @elliptic_curve.setter def elliptic_curve(self, ecurve: str) -> None: """Set the elliptic_curve property""" self._mark_runtime_override('elliptic_curve') self.namespace.elliptic_curve = ecurve @property def key_type(self) -> str: """Type of generated private key. Only *ONE* per invocation can be provided at this time. """ return self.namespace.key_type @key_type.setter def key_type(self, ktype: str) -> None: """Set the key_type property""" self._mark_runtime_override('key_type') self.namespace.key_type = ktype @property def must_staple(self) -> bool: """Adds the OCSP Must-Staple extension to the certificate. Autoconfigures OCSP Stapling for supported setups (Apache version >= 2.3.3 ). """ return self.namespace.must_staple @property def config_dir(self) -> str: """Configuration directory.""" return self.namespace.config_dir @property def work_dir(self) -> str: """Working directory.""" return self.namespace.work_dir @property def accounts_dir(self) -> str: """Directory where all account information is stored.""" return self.accounts_dir_for_server_path(self.server_path) @property def backup_dir(self) -> str: """Configuration backups directory.""" return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR) @property def csr_dir(self) -> str: """Directory where new Certificate Signing Requests (CSRs) are saved.""" warnings.warn("NamespaceConfig.csr_dir is deprecated and will be removed in an upcoming " "release of Certbot", DeprecationWarning) return os.path.join(self.namespace.config_dir, constants.CSR_DIR) @property def in_progress_dir(self) -> str: """Directory used before a permanent checkpoint is finalized.""" return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR) @property def key_dir(self) -> str: """Keys storage.""" warnings.warn("NamespaceConfig.key_dir is deprecated and will be removed in an upcoming " "release of Certbot", DeprecationWarning) return os.path.join(self.namespace.config_dir, constants.KEY_DIR) @property def temp_checkpoint_dir(self) -> str: """Temporary checkpoint directory.""" return os.path.join( self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) @property def no_verify_ssl(self) -> bool: """Disable verification of the ACME server's certificate. The root certificates trusted by Certbot can be overriden by setting the REQUESTS_CA_BUNDLE environment variable. """ return self.namespace.no_verify_ssl @property def http01_port(self) -> int: """Port used in the http-01 challenge. This only affects the port Certbot listens on. A conforming ACME server will still attempt to connect on port 80. """ return self.namespace.http01_port @property def http01_address(self) -> str: """The address the server listens to during http-01 challenge.""" return self.namespace.http01_address @property def https_port(self) -> int: """Port used to serve HTTPS. This affects which port Nginx will listen on after a LE certificate is installed. """ return self.namespace.https_port @property def pref_challs(self) -> List[str]: """List of user specified preferred challenges. Sorted with the most preferred challenge listed first. """ return self.namespace.pref_challs @property def allow_subset_of_names(self) -> bool: """Allow only a subset of names to be authorized to perform validations. When performing domain validation, do not consider it a failure if authorizations can not be obtained for a strict subset of the requested domains. This may be useful for allowing renewals for multiple domains to succeed even if some domains no longer point at this system. """ return self.namespace.allow_subset_of_names @property def strict_permissions(self) -> bool: """Enable strict permissions checks. Require that all configuration files are owned by the current user; only needed if your config is somewhere unsafe like /tmp/. """ return self.namespace.strict_permissions @property def disable_renew_updates(self) -> bool: """Disable renewal updates. If updates provided by installer enhancements when Certbot is being run with \"renew\" verb should be disabled. """ return self.namespace.disable_renew_updates @property def preferred_chain(self) -> Optional[str]: """Set the preferred certificate chain. If the CA offers multiple certificate chains, prefer the chain whose topmost certificate was issued from this Subject Common Name. If no match, the default offered chain will be used. """ return self.namespace.preferred_chain @property def server_path(self) -> str: """File path based on ``server``.""" parsed = parse.urlparse(self.namespace.server) return (parsed.netloc + parsed.path).replace('/', os.path.sep) def accounts_dir_for_server_path(self, server_path: str) -> str: """Path to accounts directory based on server_path""" server_path = misc.underscores_for_unsupported_characters_in_path(server_path) return os.path.join( self.namespace.config_dir, constants.ACCOUNTS_DIR, server_path) @property def default_archive_dir(self) -> str: # pylint: disable=missing-function-docstring return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR) @property def live_dir(self) -> str: # pylint: disable=missing-function-docstring return os.path.join(self.namespace.config_dir, constants.LIVE_DIR) @property def renewal_configs_dir(self) -> str: # pylint: disable=missing-function-docstring return os.path.join( self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR) @property def renewal_hooks_dir(self) -> str: """Path to directory with hooks to run with the renew subcommand.""" return os.path.join(self.namespace.config_dir, constants.RENEWAL_HOOKS_DIR) @property def renewal_pre_hooks_dir(self) -> str: """Path to the pre-hook directory for the renew subcommand.""" return os.path.join(self.renewal_hooks_dir, constants.RENEWAL_PRE_HOOKS_DIR) @property def renewal_deploy_hooks_dir(self) -> str: """Path to the deploy-hook directory for the renew subcommand.""" return os.path.join(self.renewal_hooks_dir, constants.RENEWAL_DEPLOY_HOOKS_DIR) @property def renewal_post_hooks_dir(self) -> str: """Path to the post-hook directory for the renew subcommand.""" return os.path.join(self.renewal_hooks_dir, constants.RENEWAL_POST_HOOKS_DIR) @property def issuance_timeout(self) -> int: """This option specifies how long (in seconds) Certbot will wait for the server to issue a certificate. """ return self.namespace.issuance_timeout @property def new_key(self) -> bool: """This option specifies whether Certbot should generate a new private key when replacing a certificate, even if reuse_key is set. """ return self.namespace.new_key # Magic methods def __deepcopy__(self, _memo: Any) -> 'NamespaceConfig': # Work around https://bugs.python.org/issue1515 for py26 tests :( :( new_ns = copy.deepcopy(self.namespace) new_config = type(self)(new_ns) # Avoid recursion loop because of the delegation defined in __setattr__ object.__setattr__(new_config, '_argument_sources', copy.deepcopy(self.argument_sources)) object.__setattr__(new_config, '_previously_accessed_mutables', copy.deepcopy(self._previously_accessed_mutables)) return new_config def _check_config_sanity(config: NamespaceConfig) -> None: """Validate command line options and display error message if requirements are not met. :param config: NamespaceConfig instance holding user configuration :type args: :class:`certbot.configuration.NamespaceConfig` """ # Port check if config.http01_port == config.https_port: raise errors.ConfigurationError( "Trying to run http-01 and https-port " "on the same port ({0})".format(config.https_port)) # Domain checks if config.namespace.domains is not None: for domain in config.namespace.domains: # This may be redundant, but let's be paranoid util.enforce_domain_sanity(domain) def _is_immutable(value: Any) -> bool: """Is value of an immutable type?""" if isinstance(value, tuple): # tuples are only immutable if all contained values are immutable. return all(_is_immutable(subvalue) for subvalue in value) for immutable_type in (int, float, complex, str, bytes, bool, frozenset,): if isinstance(value, immutable_type): return True # The last case we consider here is None which is also immutable. return value is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/crypto_util.py0000664000175100017510000006001314561227515016561 0ustar00ericaerica"""Certbot client crypto utility functions. .. todo:: Make the transition to use PSS rather than PKCS1_v1_5 when the server is capable of handling the signatures. """ import datetime import hashlib import logging import re from typing import Callable from typing import List from typing import Optional from typing import Set from typing import Tuple from typing import TYPE_CHECKING from typing import Union from cryptography import x509 from cryptography.exceptions import InvalidSignature from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey from cryptography.hazmat.primitives.asymmetric.ec import ECDSA from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from cryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives.serialization import NoEncryption from cryptography.hazmat.primitives.serialization import PrivateFormat import josepy from OpenSSL import crypto from OpenSSL import SSL import pyrfc3339 from acme import crypto_util as acme_crypto_util from certbot import errors from certbot import interfaces from certbot import util from certbot.compat import os # Cryptography ed448 and ed25519 modules do not exist on oldest tests if TYPE_CHECKING: from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.hazmat.primitives.asymmetric.x448 import X448PublicKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey logger = logging.getLogger(__name__) # High level functions def generate_key(key_size: int, key_dir: Optional[str], key_type: str = "rsa", elliptic_curve: str = "secp256r1", keyname: str = "key-certbot.pem", strict_permissions: bool = True) -> util.Key: """Initializes and saves a privkey. Inits key and saves it in PEM format on the filesystem. .. note:: keyname is the attempted filename, it may be different if a file already exists at the path. :param int key_size: key size in bits if key size is rsa. :param str key_dir: Optional key save directory. :param str key_type: Key Type [rsa, ecdsa] :param str elliptic_curve: Name of the elliptic curve if key type is ecdsa. :param str keyname: Filename of key :param bool strict_permissions: If true and key_dir exists, an exception is raised if the directory doesn't have 0700 permissions or isn't owned by the current user. :returns: Key :rtype: :class:`certbot.util.Key` :raises ValueError: If unable to generate the key given key_size. """ try: key_pem = make_key( bits=key_size, elliptic_curve=elliptic_curve or "secp256r1", key_type=key_type, ) except ValueError as err: logger.debug("", exc_info=True) logger.error("Encountered error while making key: %s", str(err)) raise err # Save file key_path = None if key_dir: util.make_or_verify_dir(key_dir, 0o700, strict_permissions) key_f, key_path = util.unique_file( os.path.join(key_dir, keyname), 0o600, "wb") with key_f: key_f.write(key_pem) if key_type == 'rsa': logger.debug("Generating RSA key (%d bits): %s", key_size, key_path) else: logger.debug("Generating ECDSA key (%d bits): %s", key_size, key_path) return util.Key(key_path, key_pem) def generate_csr(privkey: util.Key, names: Union[List[str], Set[str]], path: Optional[str], must_staple: bool = False, strict_permissions: bool = True) -> util.CSR: """Initialize a CSR with the given private key. :param privkey: Key to include in the CSR :type privkey: :class:`certbot.util.Key` :param set names: `str` names to include in the CSR :param str path: Optional certificate save directory. :param bool must_staple: If true, include the TLS Feature extension "OCSP Must-Staple" :param bool strict_permissions: If true and path exists, an exception is raised if the directory doesn't have 0755 permissions or isn't owned by the current user. :returns: CSR :rtype: :class:`certbot.util.CSR` """ csr_pem = acme_crypto_util.make_csr( privkey.pem, names, must_staple=must_staple) # Save CSR, if requested csr_filename = None if path: util.make_or_verify_dir(path, 0o755, strict_permissions) csr_f, csr_filename = util.unique_file( os.path.join(path, "csr-certbot.pem"), 0o644, "wb") with csr_f: csr_f.write(csr_pem) logger.debug("Creating CSR: %s", csr_filename) return util.CSR(csr_filename, csr_pem, "pem") # WARNING: the csr and private key file are possible attack vectors for TOCTOU # We should either... # A. Do more checks to verify that the CSR is trusted/valid # B. Audit the parsing code for vulnerabilities def valid_csr(csr: bytes) -> bool: """Validate CSR. Check if `csr` is a valid CSR for the given domains. :param bytes csr: CSR in PEM. :returns: Validity of CSR. :rtype: bool """ try: req = crypto.load_certificate_request( crypto.FILETYPE_PEM, csr) return req.verify(req.get_pubkey()) except crypto.Error: logger.debug("", exc_info=True) return False def csr_matches_pubkey(csr: bytes, privkey: bytes) -> bool: """Does private key correspond to the subject public key in the CSR? :param bytes csr: CSR in PEM. :param bytes privkey: Private key file contents (PEM) :returns: Correspondence of private key to CSR subject public key. :rtype: bool """ req = crypto.load_certificate_request( crypto.FILETYPE_PEM, csr) pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, privkey) try: return req.verify(pkey) except crypto.Error: logger.debug("", exc_info=True) return False def import_csr_file(csrfile: str, data: bytes) -> Tuple[int, util.CSR, List[str]]: """Import a CSR file, which can be either PEM or DER. :param str csrfile: CSR filename :param bytes data: contents of the CSR file :returns: (`crypto.FILETYPE_PEM`, util.CSR object representing the CSR, list of domains requested in the CSR) :rtype: tuple """ PEM = crypto.FILETYPE_PEM load = crypto.load_certificate_request try: # Try to parse as DER first, then fall back to PEM. csr = load(crypto.FILETYPE_ASN1, data) except crypto.Error: try: csr = load(PEM, data) except crypto.Error: raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) domains = _get_names_from_loaded_cert_or_req(csr) # Internally we always use PEM, so re-encode as PEM before returning. data_pem = crypto.dump_certificate_request(PEM, csr) return PEM, util.CSR(file=csrfile, data=data_pem, form="pem"), domains def make_key(bits: int = 2048, key_type: str = "rsa", elliptic_curve: Optional[str] = None) -> bytes: """Generate PEM encoded RSA|EC key. :param int bits: Number of bits if key_type=rsa. At least 2048 for RSA. :param str key_type: The type of key to generate, but be rsa or ecdsa :param str elliptic_curve: The elliptic curve to use. :returns: new RSA or ECDSA key in PEM form with specified number of bits or of type ec_curve when key_type ecdsa is used. :rtype: str """ if key_type == 'rsa': if bits < 2048: raise errors.Error("Unsupported RSA key length: {}".format(bits)) key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, bits) elif key_type == 'ecdsa': if not elliptic_curve: raise errors.Error("When key_type == ecdsa, elliptic_curve must be set.") try: name = elliptic_curve.upper() if name in ('SECP256R1', 'SECP384R1', 'SECP521R1'): curve = getattr(ec, elliptic_curve.upper()) if not curve: raise errors.Error(f"Invalid curve type: {elliptic_curve}") _key = ec.generate_private_key( curve=curve(), backend=default_backend() ) else: raise errors.Error("Unsupported elliptic curve: {}".format(elliptic_curve)) except TypeError: raise errors.Error("Unsupported elliptic curve: {}".format(elliptic_curve)) except UnsupportedAlgorithm as e: raise e from errors.Error(str(e)) _key_pem = _key.private_bytes( encoding=Encoding.PEM, format=PrivateFormat.TraditionalOpenSSL, encryption_algorithm=NoEncryption() ) key = crypto.load_privatekey(crypto.FILETYPE_PEM, _key_pem) else: raise errors.Error("Invalid key_type specified: {}. Use [rsa|ecdsa]".format(key_type)) return crypto.dump_privatekey(crypto.FILETYPE_PEM, key) def valid_privkey(privkey: Union[str, bytes]) -> bool: """Is valid RSA private key? :param privkey: Private key file contents in PEM :returns: Validity of private key. :rtype: bool """ try: return crypto.load_privatekey( crypto.FILETYPE_PEM, privkey).check() except (TypeError, crypto.Error): return False def verify_renewable_cert(renewable_cert: interfaces.RenewableCert) -> None: """For checking that your certs were not corrupted on disk. Several things are checked: 1. Signature verification for the cert. 2. That fullchain matches cert and chain when concatenated. 3. Check that the private key matches the certificate. :param renewable_cert: cert to verify :type renewable_cert: certbot.interfaces.RenewableCert :raises errors.Error: If verification fails. """ verify_renewable_cert_sig(renewable_cert) verify_fullchain(renewable_cert) verify_cert_matches_priv_key(renewable_cert.cert_path, renewable_cert.key_path) 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() assert cert.signature_hash_algorithm # always present for RSA and ECDSA 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 verify_signed_payload(public_key: Union[DSAPublicKey, 'Ed25519PublicKey', 'Ed448PublicKey', EllipticCurvePublicKey, RSAPublicKey, 'X25519PublicKey', 'X448PublicKey'], 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 verify_cert_matches_priv_key(cert_path: str, key_path: str) -> None: """ 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 certificate 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 verify_fullchain(renewable_cert: interfaces.RenewableCert) -> None: """ Verifies that fullchain is indeed cert concatenated with chain. :param renewable_cert: cert to verify :type renewable_cert: certbot.interfaces.RenewableCert :raises errors.Error: If cert and chain do not combine to fullchain. """ try: with open(renewable_cert.chain_path) as chain_file: chain = chain_file.read() with open(renewable_cert.cert_path) as cert_file: cert = cert_file.read() with open(renewable_cert.fullchain_path) as fullchain_file: fullchain = fullchain_file.read() if (cert + chain) != fullchain: error_str = "fullchain does not match cert + chain for {0}!" error_str = error_str.format(renewable_cert.lineagename) raise errors.Error(error_str) except IOError as e: error_str = "reading one of cert, chain, or fullchain has failed: {0}".format(e) logger.exception(error_str) raise errors.Error(error_str) except errors.Error as e: raise e def pyopenssl_load_certificate(data: bytes) -> Tuple[crypto.X509, int]: """Load PEM/DER certificate. :raises errors.Error: """ openssl_errors = [] for file_type in (crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1): try: return crypto.load_certificate(file_type, data), file_type except crypto.Error as error: # TODO: other errors? openssl_errors.append(error) raise errors.Error("Unable to load: {0}".format(",".join( str(error) for error in openssl_errors))) def _load_cert_or_req(cert_or_req_str: bytes, load_func: Callable[[int, bytes], Union[crypto.X509, crypto.X509Req]], typ: int = crypto.FILETYPE_PEM) -> Union[crypto.X509, crypto.X509Req]: try: return load_func(typ, cert_or_req_str) except crypto.Error as err: logger.debug("", exc_info=True) logger.error("Encountered error while loading certificate or csr: %s", str(err)) raise def _get_sans_from_cert_or_req(cert_or_req_str: bytes, load_func: Callable[[int, bytes], Union[crypto.X509, crypto.X509Req]], typ: int = crypto.FILETYPE_PEM) -> List[str]: # pylint: disable=protected-access return acme_crypto_util._pyopenssl_cert_or_req_san(_load_cert_or_req( cert_or_req_str, load_func, typ)) def get_sans_from_cert(cert: bytes, typ: int = crypto.FILETYPE_PEM) -> List[str]: """Get a list of Subject Alternative Names from a certificate. :param str cert: Certificate (encoded). :param typ: `crypto.FILETYPE_PEM` or `crypto.FILETYPE_ASN1` :returns: A list of Subject Alternative Names. :rtype: list """ return _get_sans_from_cert_or_req( cert, crypto.load_certificate, typ) def _get_names_from_cert_or_req(cert_or_req: bytes, load_func: Callable[[int, bytes], Union[crypto.X509, crypto.X509Req]], typ: int) -> List[str]: loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ) return _get_names_from_loaded_cert_or_req(loaded_cert_or_req) def _get_names_from_loaded_cert_or_req(loaded_cert_or_req: Union[crypto.X509, crypto.X509Req] ) -> List[str]: # pylint: disable=protected-access return acme_crypto_util._pyopenssl_cert_or_req_all_names(loaded_cert_or_req) def get_names_from_cert(cert: bytes, typ: int = crypto.FILETYPE_PEM) -> List[str]: """Get a list of domains from a cert, including the CN if it is set. :param str cert: Certificate (encoded). :param typ: `crypto.FILETYPE_PEM` or `crypto.FILETYPE_ASN1` :returns: A list of domain names. :rtype: list """ return _get_names_from_cert_or_req( cert, crypto.load_certificate, typ) def get_names_from_req(csr: bytes, typ: int = crypto.FILETYPE_PEM) -> List[str]: """Get a list of domains from a CSR, including the CN if it is set. :param str csr: CSR (encoded). :param typ: `crypto.FILETYPE_PEM` or `crypto.FILETYPE_ASN1` :returns: A list of domain names. :rtype: list """ return _get_names_from_cert_or_req(csr, crypto.load_certificate_request, typ) def dump_pyopenssl_chain(chain: Union[List[crypto.X509], List[josepy.ComparableX509]], filetype: int = crypto.FILETYPE_PEM) -> bytes: """Dump certificate chain into a bundle. :param list chain: List of `crypto.X509` (or wrapped in :class:`josepy.util.ComparableX509`). """ # XXX: returns empty string when no chain is available, which # shuts up RenewableCert, but might not be the best solution... return acme_crypto_util.dump_pyopenssl_chain(chain, filetype) def notBefore(cert_path: str) -> datetime.datetime: """When does the cert at cert_path start being valid? :param str cert_path: path to a cert in PEM format :returns: the notBefore value from the cert at cert_path :rtype: :class:`datetime.datetime` """ return _notAfterBefore(cert_path, crypto.X509.get_notBefore) def notAfter(cert_path: str) -> datetime.datetime: """When does the cert at cert_path stop being valid? :param str cert_path: path to a cert in PEM format :returns: the notAfter value from the cert at cert_path :rtype: :class:`datetime.datetime` """ return _notAfterBefore(cert_path, crypto.X509.get_notAfter) def _notAfterBefore(cert_path: str, method: Callable[[crypto.X509], Optional[bytes]]) -> datetime.datetime: """Internal helper function for finding notbefore/notafter. :param str cert_path: path to a cert in PEM format :param function method: one of ``crypto.X509.get_notBefore`` or ``crypto.X509.get_notAfter`` :returns: the notBefore or notAfter value from the cert at cert_path :rtype: :class:`datetime.datetime` """ # pylint: disable=redefined-outer-name with open(cert_path, "rb") as f: x509 = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) # pyopenssl always returns bytes timestamp = method(x509) if not timestamp: raise errors.Error("Error while invoking timestamp method, None has been returned.") reformatted_timestamp = [timestamp[0:4], b"-", timestamp[4:6], b"-", timestamp[6:8], b"T", timestamp[8:10], b":", timestamp[10:12], b":", timestamp[12:]] # pyrfc3339 always uses the type `str` timestamp_bytes = b"".join(reformatted_timestamp) timestamp_str = timestamp_bytes.decode('ascii') return pyrfc3339.parse(timestamp_str) def sha256sum(filename: str) -> str: """Compute a sha256sum of a file. NB: In given file, platform specific newlines characters will be converted into their equivalent unicode counterparts before calculating the hash. :param str filename: path to the file whose hash will be computed :returns: sha256 digest of the file in hexadecimal :rtype: str """ sha256 = hashlib.sha256() with open(filename, 'r') as file_d: sha256.update(file_d.read().encode('UTF-8')) return sha256.hexdigest() # Finds one CERTIFICATE stricttextualmsg according to rfc7468#section-3. # Does not validate the base64text - use crypto.load_certificate. CERT_PEM_REGEX = re.compile( b"""-----BEGIN CERTIFICATE-----\r? .+?\r? -----END CERTIFICATE-----\r? """, re.DOTALL # DOTALL (/s) because the base64text may include newlines ) def cert_and_chain_from_fullchain(fullchain_pem: str) -> Tuple[str, str]: """Split fullchain_pem into cert_pem and chain_pem :param str fullchain_pem: concatenated cert + chain :returns: tuple of string cert_pem and chain_pem :rtype: tuple :raises errors.Error: If there are less than 2 certificates in the chain. """ # First pass: find the boundary of each certificate in the chain. # TODO: This will silently skip over any "explanatory text" in between boundaries, # which is prohibited by RFC8555. certs = CERT_PEM_REGEX.findall(fullchain_pem.encode()) if len(certs) < 2: raise errors.Error("failed to parse fullchain into cert and chain: " + "less than 2 certificates in chain") # Second pass: for each certificate found, parse it using OpenSSL and re-encode it, # with the effect of normalizing any encoding variations (e.g. CRLF, whitespace). certs_normalized = [crypto.dump_certificate(crypto.FILETYPE_PEM, crypto.load_certificate(crypto.FILETYPE_PEM, cert)).decode() for cert in certs] # Since each normalized cert has a newline suffix, no extra newlines are required. return (certs_normalized[0], "".join(certs_normalized[1:])) def get_serial_from_cert(cert_path: str) -> int: """Retrieve the serial number of a certificate from certificate path :param str cert_path: path to a cert in PEM format :returns: serial number of the certificate :rtype: int """ # pylint: disable=redefined-outer-name with open(cert_path, "rb") as f: x509 = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) return x509.get_serial_number() def find_chain_with_issuer(fullchains: List[str], issuer_cn: str, warn_on_no_match: bool = False) -> str: """Chooses the first certificate chain from fullchains whose topmost intermediate has an Issuer Common Name matching issuer_cn (in other words the first chain which chains to a root whose name matches issuer_cn). :param fullchains: The list of fullchains in PEM chain format. :type fullchains: `list` of `str` :param `str` issuer_cn: The exact Subject Common Name to match against any issuer in the certificate chain. :returns: The best-matching fullchain, PEM-encoded, or the first if none match. :rtype: `str` """ for chain in fullchains: certs = CERT_PEM_REGEX.findall(chain.encode()) top_cert = x509.load_pem_x509_certificate(certs[-1], default_backend()) top_issuer_cn = top_cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME) if top_issuer_cn and top_issuer_cn[0].value == issuer_cn: return chain # Nothing matched, return whatever was first in the list. if warn_on_no_match: logger.warning("Certbot has been configured to prefer certificate chains with " "issuer '%s', but no chain from the CA matched this issuer. Using " "the default certificate chain instead.", issuer_cn) return fullchains[0] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3350835 certbot-2.9.0/certbot/display/0000775000175100017510000000000014561227516015300 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/display/__init__.py0000664000175100017510000000004114561227515017403 0ustar00ericaerica"""Certbot display utilities.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/display/ops.py0000664000175100017510000003232214561227515016454 0ustar00ericaerica"""Contains UI methods for LE user operations.""" import logging from textwrap import indent from typing import Any from typing import Callable from typing import Iterable from typing import List from typing import Optional from typing import Tuple from certbot import errors from certbot import interfaces from certbot import util from certbot._internal import account from certbot._internal.display import util as internal_display_util from certbot.compat import os from certbot.display import util as display_util logger = logging.getLogger(__name__) def get_email(invalid: bool = False, optional: bool = True) -> str: """Prompt for valid email address. :param bool invalid: True if an invalid address was provided by the user :param bool optional: True if the user can use --register-unsafely-without-email to avoid providing an e-mail :returns: e-mail address :rtype: str :raises errors.Error: if the user cancels """ invalid_prefix = "There seem to be problems with that address. " msg = "Enter email address (used for urgent renewal and security notices)\n" unsafe_suggestion = ("\n\nIf you really want to skip this, you can run " "the client with --register-unsafely-without-email " "but you will then be unable to receive notice about " "impending expiration or revocation of your " "certificates or problems with your Certbot " "installation that will lead to failure to renew.\n\n") if optional: if invalid: msg += unsafe_suggestion suggest_unsafe = False else: suggest_unsafe = True else: suggest_unsafe = False while True: try: code, email = display_util.input_text(invalid_prefix + msg if invalid else msg, force_interactive=True) except errors.MissingCommandlineFlag: msg = ("You should register before running non-interactively, " "or provide --agree-tos and --email flags.") raise errors.MissingCommandlineFlag(msg) if code != display_util.OK: if optional: raise errors.Error( "An e-mail address or " "--register-unsafely-without-email must be provided.") raise errors.Error("An e-mail address must be provided.") if util.safe_email(email): return email if suggest_unsafe: msg = unsafe_suggestion + msg suggest_unsafe = False # add this message at most once invalid = bool(email) def choose_account(accounts: List[account.Account]) -> Optional[account.Account]: """Choose an account. :param list accounts: Containing at least one :class:`~certbot._internal.account.Account` """ # Note this will get more complicated once we start recording authorizations labels = [acc.slug for acc in accounts] code, index = display_util.menu("Please choose an account", labels, force_interactive=True) if code == display_util.OK: return accounts[index] return None def choose_values(values: List[str], question: Optional[str] = None) -> List[str]: """Display screen to let user pick one or multiple values from the provided list. :param list values: Values to select from :param str question: Question to ask to user while choosing values :returns: List of selected values :rtype: list """ code, items = display_util.checklist(question if question else "", tags=values, force_interactive=True) if code == display_util.OK and items: return items return [] def choose_names(installer: Optional[interfaces.Installer], question: Optional[str] = None) -> List[str]: """Display screen to select domains to validate. :param installer: An installer object :type installer: :class:`certbot.interfaces.Installer` :param `str` question: Overriding default question to ask the user if asked to choose from domain names. :returns: List of selected names :rtype: `list` of `str` """ if installer is None: logger.debug("No installer, picking names manually") return _choose_names_manually() domains = list(installer.get_all_names()) names = get_valid_domains(domains) if not names: return _choose_names_manually() code, names = _filter_names(names, question) if code == display_util.OK and names: return names return [] def get_valid_domains(domains: Iterable[str]) -> List[str]: """Helper method for choose_names that implements basic checks on domain names :param list domains: Domain names to validate :return: List of valid domains :rtype: list """ valid_domains: List[str] = [] for domain in domains: try: valid_domains.append(util.enforce_domain_sanity(domain)) except errors.ConfigurationError: continue return valid_domains def _sort_names(FQDNs: Iterable[str]) -> List[str]: """Sort FQDNs by SLD (and if many, by their subdomains) :param list FQDNs: list of domain names :returns: Sorted list of domain names :rtype: list """ return sorted(FQDNs, key=lambda fqdn: fqdn.split('.')[::-1][1:]) def _filter_names(names: Iterable[str], override_question: Optional[str] = None) -> Tuple[str, List[str]]: """Determine which names the user would like to select from a list. :param list names: domain names :returns: tuple of the form (`code`, `names`) where `code` - str display exit code `names` - list of names selected :rtype: tuple """ # Sort by domain first, and then by subdomain sorted_names = _sort_names(names) if override_question: question = override_question else: question = ( "Which names would you like to activate HTTPS for?\n" "We recommend selecting either all domains, or all domains in a VirtualHost/server " "block.") code, names = display_util.checklist( question, tags=sorted_names, cli_flag="--domains", force_interactive=True) return code, [str(s) for s in names] def _choose_names_manually(prompt_prefix: str = "") -> List[str]: """Manually input names for those without an installer. :param str prompt_prefix: string to prepend to prompt for domains :returns: list of provided names :rtype: `list` of `str` """ code, input_ = display_util.input_text( prompt_prefix + "Please enter the domain name(s) you would like on your certificate " "(comma and/or space separated)", cli_flag="--domains", force_interactive=True) if code == display_util.OK: invalid_domains = {} retry_message = "" try: domain_list = internal_display_util.separate_list_input(input_) except UnicodeEncodeError: domain_list = [] retry_message = ( "Internationalized domain names are not presently " "supported.{0}{0}Would you like to re-enter the " "names?{0}").format(os.linesep) for i, domain in enumerate(domain_list): try: domain_list[i] = util.enforce_domain_sanity(domain) except errors.ConfigurationError as e: invalid_domains[domain] = str(e) if invalid_domains: retry_message = ( "One or more of the entered domain names was not valid:" "{0}{0}").format(os.linesep) for invalid_domain, err in invalid_domains.items(): retry_message = retry_message + "{1}: {2}{0}".format( os.linesep, invalid_domain, err) retry_message = retry_message + ( "{0}Would you like to re-enter the names?{0}").format( os.linesep) if retry_message: # We had error in input retry = display_util.yesno(retry_message, force_interactive=True) if retry: return _choose_names_manually() else: return domain_list return [] def success_installation(domains: List[str]) -> None: """Display a box confirming the installation of HTTPS. :param list domains: domain names which were enabled """ display_util.notify( "Congratulations! You have successfully enabled HTTPS on {0}" .format(_gen_https_names(domains)) ) def success_renewal(unused_domains: List[str]) -> None: """Display a box confirming the renewal of an existing certificate. :param list domains: domain names which were renewed """ display_util.notify( "Your existing certificate has been successfully renewed, and the " "new certificate has been installed." ) def success_revocation(cert_path: str) -> None: """Display a message confirming a certificate has been revoked. :param list cert_path: path to certificate which was revoked. """ display_util.notify( "Congratulations! You have successfully revoked the certificate " "that was located at {0}.".format(cert_path) ) def report_executed_command(command_name: str, returncode: int, stdout: str, stderr: str) -> None: """Display a message describing the success or failure of an executed process (e.g. hook). :param str command_name: Human-readable description of the executed command :param int returncode: The exit code of the executed command :param str stdout: The stdout output of the executed command :param str stderr: The stderr output of the executed command """ out_s, err_s = stdout.strip(), stderr.strip() if returncode != 0: logger.warning("%s reported error code %d", command_name, returncode) if out_s: display_util.notify(f"{command_name} ran with output:\n{indent(out_s, ' ')}") if err_s: logger.warning("%s ran with error output:\n%s", command_name, indent(err_s, ' ')) def _gen_https_names(domains: List[str]) -> str: """Returns a string of the https domains. Domains are formatted nicely with ``https://`` prepended to each. :param list domains: Each domain is a 'str' """ if len(domains) == 1: return "https://{0}".format(domains[0]) elif len(domains) == 2: return "https://{dom[0]} and https://{dom[1]}".format(dom=domains) elif len(domains) > 2: return "{0}{1}{2}".format( ", ".join("https://%s" % dom for dom in domains[:-1]), ", and https://", domains[-1]) return "" def _get_validated(method: Callable[..., Tuple[str, str]], validator: Callable[[str], Any], message: str, default: Optional[str] = None, **kwargs: Any) -> Tuple[str, str]: if default is not None: try: validator(default) except errors.Error: logger.debug('Encountered invalid default value "%s" when prompting for "%s"', default, message, exc_info=True) raise AssertionError('Invalid default "{0}"'.format(default)) while True: code, raw = method(message, default=default, **kwargs) if code == display_util.OK: try: validator(raw) return code, raw except errors.Error as error: logger.debug('Validator rejected "%s" when prompting for "%s"', raw, message, exc_info=True) display_util.notification(str(error), pause=False) else: return code, raw def validated_input(validator: Callable[[str], Any], *args: Any, **kwargs: Any) -> Tuple[str, str]: """Like `~certbot.display.util.input_text`, but with validation. :param callable validator: A method which will be called on the supplied input. If the method raises an `errors.Error`, its text will be displayed and the user will be re-prompted. :param list `*args`: Arguments to be passed to `~certbot.display.util.input_text`. :param dict `**kwargs`: Arguments to be passed to `~certbot.display.util.input_text`. :return: as `~certbot.display.util.input_text` :rtype: tuple """ return _get_validated(display_util.input_text, validator, *args, **kwargs) def validated_directory(validator: Callable[[str], Any], *args: Any, **kwargs: Any) -> Tuple[str, str]: """Like `~certbot.display.util.directory_select`, but with validation. :param callable validator: A method which will be called on the supplied input. If the method raises an `errors.Error`, its text will be displayed and the user will be re-prompted. :param list `*args`: Arguments to be passed to `~certbot.display.util.directory_select`. :param dict `**kwargs`: Arguments to be passed to `~certbot.display.util.directory_select`. :return: as `~certbot.display.util.directory_select` :rtype: tuple """ return _get_validated(display_util.directory_select, validator, *args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/display/util.py0000664000175100017510000001614414561227515016634 0ustar00ericaerica"""Certbot display. This module (`certbot.display.util`) or its companion `certbot.display.ops` should be used whenever: - Displaying status information to the user on the terminal - Collecting information from the user via prompts Other messages can use the `logging` module. See `log.py`. """ from typing import List from typing import Optional from typing import Tuple from typing import Union from certbot._internal.display import obj # These constants are defined this way to make them easier to document with # Sphinx and to not couple our public docstrings to our internal ones. OK = obj.OK """Display exit code indicating user acceptance.""" CANCEL = obj.CANCEL """Display exit code for a user canceling the display.""" WIDTH = 72 def notify(msg: str) -> None: """Display a basic status message. :param str msg: message to display """ obj.get_display().notification(msg, pause=False, decorate=False, wrap=False) def notification(message: str, pause: bool = True, wrap: bool = True, force_interactive: bool = False, decorate: bool = True) -> None: """Displays a notification and waits for user acceptance. :param str message: Message to display :param bool pause: Whether or not the program should pause for the user's confirmation :param bool wrap: Whether or not the application should wrap text :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :param bool decorate: Whether to surround the message with a decorated frame """ obj.get_display().notification(message, pause=pause, wrap=wrap, force_interactive=force_interactive, decorate=decorate) def menu(message: str, choices: Union[List[str], List[Tuple[str, str]]], default: Optional[int] = None, cli_flag: Optional[str] = None, force_interactive: bool = False) -> Tuple[str, int]: """Display a menu. .. todo:: This doesn't enable the help label/button (I wasn't sold on any interface I came up with for this). It would be a nice feature. :param str message: title of menu :param choices: Menu lines, len must be > 0 :type choices: list of tuples (tag, item) or list of descriptions (tags will be enumerated) :param default: default value to return, if interaction is not possible :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: tuple of (`code`, `index`) where `code` - str display exit code `index` - int index of the user's selection :rtype: tuple """ return obj.get_display().menu(message, choices, default=default, cli_flag=cli_flag, force_interactive=force_interactive) def input_text(message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, force_interactive: bool = False) -> Tuple[str, str]: """Accept input from the user. :param str message: message to display to the user :param default: default value to return, if interaction is not possible :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: tuple of (`code`, `input`) where `code` - str display exit code `input` - str of the user's input :rtype: tuple """ return obj.get_display().input(message, default=default, cli_flag=cli_flag, force_interactive=force_interactive) def yesno(message: str, yes_label: str = "Yes", no_label: str = "No", default: Optional[bool] = None, cli_flag: Optional[str] = None, force_interactive: bool = False) -> bool: """Query the user with a yes/no question. Yes and No label must begin with different letters, and must contain at least one letter each. :param str message: question for the user :param str yes_label: Label of the "Yes" parameter :param str no_label: Label of the "No" parameter :param default: default value to return, if interaction is not possible :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: True for "Yes", False for "No" :rtype: bool """ return obj.get_display().yesno(message, yes_label=yes_label, no_label=no_label, default=default, cli_flag=cli_flag, force_interactive=force_interactive) def checklist(message: str, tags: List[str], default: Optional[List[str]] = None, cli_flag: Optional[str] = None, force_interactive: bool = False) -> Tuple[str, List[str]]: """Display a checklist. :param str message: Message to display to user :param list tags: `str` tags to select, len(tags) > 0 :param default: default value to return, if interaction is not possible :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: tuple of (`code`, `tags`) where `code` - str display exit code `tags` - list of selected tags :rtype: tuple """ return obj.get_display().checklist(message, tags, default=default, cli_flag=cli_flag, force_interactive=force_interactive) def directory_select(message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, force_interactive: bool = False) -> Tuple[str, str]: """Display a directory selection screen. :param str message: prompt to give the user :param default: default value to return, if interaction is not possible :param str cli_flag: option used to set this value with the CLI :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions :returns: tuple of the form (`code`, `string`) where `code` - display exit code `string` - input entered by the user """ return obj.get_display().directory_select(message, default=default, cli_flag=cli_flag, force_interactive=force_interactive) def assert_valid_call(prompt: str, default: str, cli_flag: str, force_interactive: bool) -> None: """Verify that provided arguments is a valid display call. :param str prompt: prompt for the user :param default: default answer to prompt :param str cli_flag: command line option for setting an answer to this question :param bool force_interactive: if interactivity is forced """ msg = "Invalid display call for this prompt:\n{0}".format(prompt) if cli_flag: msg += ("\nYou can set an answer to " "this prompt with the {0} flag".format(cli_flag)) assert default is not None or force_interactive, msg ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/errors.py0000664000175100017510000000534214561227515015524 0ustar00ericaerica"""Certbot client errors.""" from typing import Set from typing import TYPE_CHECKING if TYPE_CHECKING: from certbot.achallenges import AnnotatedChallenge class Error(Exception): """Generic Certbot client error.""" class AccountStorageError(Error): """Generic `.AccountStorage` error.""" class AccountNotFound(AccountStorageError): """Account not found error.""" class ReverterError(Error): """Certbot Reverter error.""" class SubprocessError(Error): """Subprocess handling error.""" class CertStorageError(Error): """Generic `.CertStorage` error.""" class HookCommandNotFound(Error): """Failed to find a hook command in the PATH.""" class SignalExit(Error): """A Unix signal was received while in the ErrorHandler context manager.""" class OverlappingMatchFound(Error): """Multiple lineages matched what should have been a unique result.""" class LockError(Error): """File locking error.""" # Auth Handler Errors class AuthorizationError(Error): """Authorization error.""" class FailedChallenges(AuthorizationError): """Failed challenges error. :ivar set failed_achalls: Failed `.AnnotatedChallenge` instances. """ def __init__(self, failed_achalls: Set['AnnotatedChallenge']) -> None: assert failed_achalls self.failed_achalls = failed_achalls super().__init__() def __str__(self) -> str: return "Failed authorization procedure. {0}".format( ", ".join( "{0} ({1}): {2}".format(achall.domain, achall.typ, achall.error) for achall in self.failed_achalls if achall.error is not None)) # Plugin Errors class PluginError(Error): """Certbot Plugin error.""" class PluginEnhancementAlreadyPresent(Error): """ Enhancement was already set """ class PluginSelectionError(Error): """A problem with plugin/configurator selection or setup""" class NoInstallationError(PluginError): """Certbot No Installation error.""" class MisconfigurationError(PluginError): """Certbot Misconfiguration error.""" class NotSupportedError(PluginError): """Certbot Plugin function not supported error.""" class PluginStorageError(PluginError): """Certbot Plugin Storage error.""" class StandaloneBindError(Error): """Standalone plugin bind error.""" def __init__(self, socket_error: OSError, port: int) -> None: super().__init__( "Problem binding to port {0}: {1}".format(port, socket_error)) self.socket_error = socket_error self.port = port class ConfigurationError(Error): """Configuration sanity error.""" # NoninteractiveDisplay error: class MissingCommandlineFlag(Error): """A command line argument was missing in noninteractive usage""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/interfaces.py0000664000175100017510000003634014561227515016335 0ustar00ericaerica"""Certbot client interfaces.""" from abc import ABCMeta from abc import abstractmethod from argparse import ArgumentParser from typing import Any from typing import Iterable from typing import List from typing import Optional from typing import Type from typing import TYPE_CHECKING from typing import Union from acme.challenges import Challenge from acme.challenges import ChallengeResponse from acme.client import ClientV2 from certbot import configuration from certbot.achallenges import AnnotatedChallenge try: from zope.interface import Interface as ZopeInterface except ImportError: ZopeInterface = object if TYPE_CHECKING: from certbot._internal.account import Account class AccountStorage(metaclass=ABCMeta): """Accounts storage interface.""" @abstractmethod def find_all(self) -> List['Account']: # pragma: no cover """Find all accounts. :returns: All found accounts. :rtype: list """ raise NotImplementedError() @abstractmethod def load(self, account_id: str) -> 'Account': # pragma: no cover """Load an account by its id. :raises .AccountNotFound: if account could not be found :raises .AccountStorageError: if account could not be loaded :returns: The account loaded :rtype: .Account """ raise NotImplementedError() @abstractmethod def save(self, account: 'Account', client: ClientV2) -> None: # pragma: no cover """Save account. :raises .AccountStorageError: if account could not be saved """ raise NotImplementedError() class Plugin(metaclass=ABCMeta): """Certbot plugin. Objects providing this interface will be called without satisfying any entry point "extras" (extra dependencies) you might have defined for your plugin, e.g (excerpt from ``setup.py`` script):: setup( ... entry_points={ 'certbot.plugins': [ 'name=example_project.plugin[plugin_deps]', ], }, extras_require={ 'plugin_deps': ['dep1', 'dep2'], } ) Therefore, make sure such objects are importable and usable without extras. This is necessary, because CLI does the following operations (in order): - loads an entry point, - calls `inject_parser_options`, - requires an entry point, - creates plugin instance (`__call__`). """ description: str = NotImplemented """Short plugin description""" name: str = NotImplemented """Unique name of the plugin""" @abstractmethod def __init__(self, config: Optional[configuration.NamespaceConfig], name: str) -> None: """Create a new `Plugin`. :param configuration.NamespaceConfig config: Configuration. :param str name: Unique plugin name. """ super().__init__() @abstractmethod def prepare(self) -> None: """Prepare the plugin. Finish up any additional initialization. :raises .PluginError: when full initialization cannot be completed. :raises .MisconfigurationError: when full initialization cannot be completed. Plugin will be displayed on a list of available plugins. :raises .NoInstallationError: when the necessary programs/files cannot be located. Plugin will NOT be displayed on a list of available plugins. :raises .NotSupportedError: when the installation is recognized, but the version is not currently supported. """ @abstractmethod def more_info(self) -> str: """Human-readable string to help the user. Should describe the steps taken and any relevant info to help the user decide which plugin to use. :rtype str: """ @classmethod @abstractmethod def inject_parser_options(cls, parser: ArgumentParser, name: str) -> None: """Inject argument parser options (flags). 1. Be nice and prepend all options and destinations with `~.common.option_namespace` and `~common.dest_namespace`. 2. Inject options (flags) only. Positional arguments are not allowed, as this would break the CLI. :param ArgumentParser parser: (Almost) top-level CLI parser. :param str name: Unique plugin name. """ class Authenticator(Plugin): """Generic Certbot Authenticator. Class represents all possible tools processes that have the ability to perform challenges and attain a certificate. """ @abstractmethod def get_chall_pref(self, domain: str) -> Iterable[Type[Challenge]]: """Return `collections.Iterable` of challenge preferences. :param str domain: Domain for which challenge preferences are sought. :returns: `collections.Iterable` of challenge types (subclasses of :class:`acme.challenges.Challenge`) with the most preferred challenges first. If a type is not specified, it means the Authenticator cannot perform the challenge. :rtype: `collections.Iterable` """ @abstractmethod def perform(self, achalls: List[AnnotatedChallenge]) -> List[ChallengeResponse]: """Perform the given challenge. :param list achalls: Non-empty (guaranteed) list of :class:`~certbot.achallenges.AnnotatedChallenge` instances, such that it contains types found within :func:`get_chall_pref` only. :returns: list of ACME :class:`~acme.challenges.ChallengeResponse` instances corresponding to each provided :class:`~acme.challenges.Challenge`. :rtype: :class:`collections.List` of :class:`acme.challenges.ChallengeResponse`, where responses are required to be returned in the same order as corresponding input challenges :raises .PluginError: If some or all challenges cannot be performed """ @abstractmethod def cleanup(self, achalls: List[AnnotatedChallenge]) -> None: """Revert changes and shutdown after challenges complete. This method should be able to revert all changes made by perform, even if perform exited abnormally. :param list achalls: Non-empty (guaranteed) list of :class:`~certbot.achallenges.AnnotatedChallenge` instances, a subset of those previously passed to :func:`perform`. :raises PluginError: if original configuration cannot be restored """ class Installer(Plugin): """Generic Certbot Installer Interface. Represents any server that an X509 certificate can be placed. It is assumed that :func:`save` is the only method that finalizes a checkpoint. This is important to ensure that checkpoints are restored in a consistent manner if requested by the user or in case of an error. Using :class:`certbot.reverter.Reverter` to implement checkpoints, rollback, and recovery can dramatically simplify plugin development. """ @abstractmethod def get_all_names(self) -> Iterable[str]: """Returns all names that may be authenticated. :rtype: `collections.Iterable` of `str` """ @abstractmethod def deploy_cert(self, domain: str, cert_path: str, key_path: str, chain_path: str, fullchain_path: str) -> None: """Deploy certificate. :param str domain: domain to deploy certificate file :param str cert_path: absolute path to the certificate file :param str key_path: absolute path to the private key file :param str chain_path: absolute path to the certificate chain file :param str fullchain_path: absolute path to the certificate fullchain file (cert plus chain) :raises .PluginError: when cert cannot be deployed """ @abstractmethod def enhance(self, domain: str, enhancement: str, options: Optional[Union[List[str], str]] = None) -> None: """Perform a configuration enhancement. :param str domain: domain for which to provide enhancement :param str enhancement: An enhancement as defined in :const:`~certbot.plugins.enhancements.ENHANCEMENTS` :param options: Flexible options parameter for enhancement. Check documentation of :const:`~certbot.plugins.enhancements.ENHANCEMENTS` for expected options for each enhancement. :raises .PluginError: If Enhancement is not supported, or if an error occurs during the enhancement. """ @abstractmethod def supported_enhancements(self) -> List[str]: """Returns a `collections.Iterable` of supported enhancements. :returns: supported enhancements which should be a subset of :const:`~certbot.plugins.enhancements.ENHANCEMENTS` :rtype: :class:`collections.Iterable` of :class:`str` """ @abstractmethod def save(self, title: Optional[str] = None, temporary: bool = False) -> None: """Saves all changes to the configuration files. Both title and temporary are needed because a save may be intended to be permanent, but the save is not ready to be a full checkpoint. It is assumed that at most one checkpoint is finalized by this method. Additionally, if an exception is raised, it is assumed a new checkpoint was not finalized. :param str 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. `title` has no effect if temporary is true. :param bool temporary: Indicates whether the changes made will be quickly reversed in the future (challenges) :raises .PluginError: when save is unsuccessful """ @abstractmethod def rollback_checkpoints(self, rollback: int = 1) -> None: """Revert `rollback` number of configuration checkpoints. :raises .PluginError: when configuration cannot be fully reverted """ @abstractmethod def recovery_routine(self) -> None: """Revert configuration to most recent finalized checkpoint. Remove all changes (temporary and permanent) that have not been finalized. This is useful to protect against crashes and other execution interruptions. :raises .errors.PluginError: If unable to recover the configuration """ @abstractmethod def config_test(self) -> None: """Make sure the configuration is valid. :raises .MisconfigurationError: when the config is not in a usable state """ @abstractmethod def restart(self) -> None: """Restart or refresh the server content. :raises .PluginError: when server cannot be restarted """ class RenewableCert(metaclass=ABCMeta): """Interface to a certificate lineage.""" @property @abstractmethod def cert_path(self) -> str: """Path to the certificate file. :rtype: str """ @property @abstractmethod def key_path(self) -> str: """Path to the private key file. :rtype: str """ @property @abstractmethod def chain_path(self) -> str: """Path to the certificate chain file. :rtype: str """ @property @abstractmethod def fullchain_path(self) -> str: """Path to the full chain file. The full chain is the certificate file plus the chain file. :rtype: str """ @property @abstractmethod def lineagename(self) -> str: """Name given to the certificate lineage. :rtype: str """ @abstractmethod def names(self) -> List[str]: """What are the subject names of this certificate? :returns: the subject names :rtype: `list` of `str` :raises .CertStorageError: if could not find cert file. """ # Updater interfaces # # When "certbot renew" is run, Certbot will iterate over each lineage and check # if the selected installer for that lineage is a subclass of each updater # class. If it is and the update of that type is configured to be run for that # lineage, the relevant update function will be called for it. These functions # are never called for other subcommands, so if an installer wants to perform # an update during the run or install subcommand, it should do so when # :func:`IInstaller.deploy_cert` is called. class GenericUpdater(metaclass=ABCMeta): """Interface for update types not currently specified by Certbot. This class allows plugins to perform types of updates that Certbot hasn't defined (yet). To make use of this interface, the installer should implement the interface methods, and interfaces.GenericUpdater.register(InstallerClass) should be called from the installer code. The plugins implementing this enhancement are responsible of handling the saving of configuration checkpoints as well as other calls to interface methods of `interfaces.Installer` such as prepare() and restart() """ @abstractmethod def generic_updates(self, lineage: RenewableCert, *args: Any, **kwargs: Any) -> None: """Perform any update types defined by the installer. If an installer is a subclass of the class containing this method, this function will always be called when "certbot renew" is run. If the update defined by the installer should be run conditionally, the installer needs to handle checking the conditions itself. This method is called once for each lineage. :param lineage: Certificate lineage object :type lineage: RenewableCert """ class RenewDeployer(metaclass=ABCMeta): """Interface for update types run when a lineage is renewed This class allows plugins to perform types of updates that need to run at lineage renewal that Certbot hasn't defined (yet). To make use of this interface, the installer should implement the interface methods, and interfaces.RenewDeployer.register(InstallerClass) should be called from the installer code. """ @abstractmethod def renew_deploy(self, lineage: RenewableCert, *args: Any, **kwargs: Any) -> None: """Perform updates defined by installer when a certificate has been renewed If an installer is a subclass of the class containing this method, this function will always be called when a certificate has been renewed by running "certbot renew". For example if a plugin needs to copy a certificate over, or change configuration based on the new certificate. This method is called once for each lineage renewed :param lineage: Certificate lineage object :type lineage: RenewableCert """ class IPluginFactory(ZopeInterface): """Compatibility shim for plugins that still use Certbot's old zope.interface classes.""" class IPlugin(ZopeInterface): """Compatibility shim for plugins that still use Certbot's old zope.interface classes.""" class IAuthenticator(IPlugin): """Compatibility shim for plugins that still use Certbot's old zope.interface classes.""" class IInstaller(IPlugin): """Compatibility shim for plugins that still use Certbot's old zope.interface classes.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/main.py0000664000175100017510000000102714561227515015130 0ustar00ericaerica"""Certbot main public entry point.""" from typing import List from typing import Optional from typing import Union from certbot._internal import main as internal_main def main(cli_args: Optional[List[str]] = None) -> Optional[Union[str, int]]: """Run Certbot. :param cli_args: command line to Certbot, defaults to ``sys.argv[1:]`` :type cli_args: `list` of `str` :returns: value for `sys.exit` about the exit status of Certbot :rtype: `str` or `int` or `None` """ return internal_main.main(cli_args) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/ocsp.py0000664000175100017510000003531414561227515015156 0ustar00ericaerica"""Tools for checking certificate revocation.""" from datetime import datetime from datetime import timedelta import logging import re import subprocess from subprocess import PIPE from typing import Optional from typing import Tuple from cryptography import x509 from cryptography.exceptions import InvalidSignature from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.x509 import ocsp import pytz import requests from certbot import crypto_util from certbot import errors from certbot import util from certbot.compat.os import getenv from certbot.interfaces import RenewableCert logger = logging.getLogger(__name__) class RevocationChecker: """This class figures out OCSP checking on this system, and performs it.""" def __init__(self, enforce_openssl_binary_usage: bool = False) -> None: self.broken = False self.use_openssl_binary = enforce_openssl_binary_usage if self.use_openssl_binary: if not util.exe_exists("openssl"): logger.info("openssl not installed, can't check revocation") self.broken = True return # New versions of openssl want -header var=val, old ones want -header var val test_host_format = subprocess.run(["openssl", "ocsp", "-header", "var", "val"], stdout=PIPE, stderr=PIPE, universal_newlines=True, check=False, env=util.env_no_snap_for_external_calls()) if "Missing =" in test_host_format.stderr: self.host_args = lambda host: ["Host=" + host] else: self.host_args = lambda host: ["Host", host] def ocsp_revoked(self, cert: RenewableCert) -> bool: """Get revoked status for a particular cert version. .. todo:: Make this a non-blocking call :param `.interfaces.RenewableCert` cert: Certificate object :returns: True if revoked; False if valid or the check failed or cert is expired. :rtype: bool """ return self.ocsp_revoked_by_paths(cert.cert_path, cert.chain_path) def ocsp_revoked_by_paths(self, cert_path: str, chain_path: str, timeout: int = 10) -> bool: """Performs the OCSP revocation check :param str cert_path: Certificate filepath :param str chain_path: Certificate chain :param int timeout: Timeout (in seconds) for the OCSP query :returns: True if revoked; False if valid or the check failed or cert is expired. :rtype: bool """ if self.broken: return False # Let's Encrypt doesn't update OCSP for expired certificates, # so don't check OCSP if the cert is expired. # https://github.com/certbot/certbot/issues/7152 now = datetime.now(pytz.UTC) if crypto_util.notAfter(cert_path) <= now: return False url, host = _determine_ocsp_server(cert_path) if not host or not url: return False if self.use_openssl_binary: return self._check_ocsp_openssl_bin(cert_path, chain_path, host, url, timeout) return _check_ocsp_cryptography(cert_path, chain_path, url, timeout) def _check_ocsp_openssl_bin(self, cert_path: str, chain_path: str, host: str, url: str, timeout: int) -> bool: # Minimal implementation of proxy selection logic as seen in, e.g., cURL # Some things that won't work, but may well be in use somewhere: # - username and password for proxy authentication # - proxies accepting TLS connections # - proxy exclusion through NO_PROXY env_http_proxy = getenv('http_proxy') env_HTTP_PROXY = getenv('HTTP_PROXY') proxy_host = None if env_http_proxy is not None or env_HTTP_PROXY is not None: proxy_host = env_http_proxy if env_http_proxy is not None else env_HTTP_PROXY if proxy_host is None: url_opts = ["-url", url] else: if proxy_host.startswith('http://'): proxy_host = proxy_host[len('http://'):] url_opts = ["-host", proxy_host, "-path", url] # jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this! cmd = ["openssl", "ocsp", "-no_nonce", "-issuer", chain_path, "-cert", cert_path, "-CAfile", chain_path, "-verify_other", chain_path, "-trust_other", "-timeout", str(timeout), "-header"] + self.host_args(host) + url_opts logger.debug("Querying OCSP for %s", cert_path) logger.debug(" ".join(cmd)) try: output, err = util.run_script(cmd, log=logger.debug) except errors.SubprocessError: logger.info("OCSP check failed for %s (are we offline?)", cert_path) return False return _translate_ocsp_query(cert_path, output, err) def _determine_ocsp_server(cert_path: str) -> Tuple[Optional[str], Optional[str]]: """Extract the OCSP server host from a certificate. :param str cert_path: Path to the cert we're checking OCSP for :rtype tuple: :returns: (OCSP server URL or None, OCSP server host or None) """ with open(cert_path, 'rb') as file_handler: cert = x509.load_pem_x509_certificate(file_handler.read(), default_backend()) try: extension = cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess) ocsp_oid = x509.AuthorityInformationAccessOID.OCSP descriptions = [description for description in extension.value if description.access_method == ocsp_oid] url = descriptions[0].access_location.value except (x509.ExtensionNotFound, IndexError): logger.info("Cannot extract OCSP URI from %s", cert_path) return None, None url = url.rstrip() host = url.partition("://")[2].rstrip("/") if host: return url, host logger.info("Cannot process OCSP host from URL (%s) in certificate at %s", url, cert_path) return None, None def _check_ocsp_cryptography(cert_path: str, chain_path: str, url: str, timeout: int) -> bool: # Retrieve OCSP response with open(chain_path, 'rb') as file_handler: issuer = x509.load_pem_x509_certificate(file_handler.read(), default_backend()) with open(cert_path, 'rb') as file_handler: cert = x509.load_pem_x509_certificate(file_handler.read(), default_backend()) builder = ocsp.OCSPRequestBuilder() builder = builder.add_certificate(cert, issuer, hashes.SHA1()) request = builder.build() request_binary = request.public_bytes(serialization.Encoding.DER) try: response = requests.post(url, data=request_binary, headers={'Content-Type': 'application/ocsp-request'}, timeout=timeout) except requests.exceptions.RequestException: logger.info("OCSP check failed for %s (are we offline?)", cert_path, exc_info=True) return False if response.status_code != 200: logger.info("OCSP check failed for %s (HTTP status: %d)", cert_path, response.status_code) return False response_ocsp = ocsp.load_der_ocsp_response(response.content) # Check OCSP response validity if response_ocsp.response_status != ocsp.OCSPResponseStatus.SUCCESSFUL: logger.warning("Invalid OCSP response status for %s: %s", cert_path, response_ocsp.response_status) return False # Check OCSP signature try: _check_ocsp_response(response_ocsp, request, issuer, cert_path) except UnsupportedAlgorithm as e: logger.warning(str(e)) except errors.Error as e: logger.warning(str(e)) except InvalidSignature: logger.warning('Invalid signature on OCSP response for %s', cert_path) except AssertionError as error: logger.warning('Invalid OCSP response for %s: %s.', cert_path, str(error)) else: # Check OCSP certificate status logger.debug("OCSP certificate status for %s is: %s", cert_path, response_ocsp.certificate_status) return response_ocsp.certificate_status == ocsp.OCSPCertStatus.REVOKED return False def _check_ocsp_response(response_ocsp: 'ocsp.OCSPResponse', request_ocsp: 'ocsp.OCSPRequest', issuer_cert: x509.Certificate, cert_path: str) -> None: """Verify that the OCSP is valid for several criteria""" # Assert OCSP response corresponds to the certificate we are talking about if response_ocsp.serial_number != request_ocsp.serial_number: raise AssertionError('the certificate in response does not correspond ' 'to the certificate in request') # Assert signature is valid _check_ocsp_response_signature(response_ocsp, issuer_cert, cert_path) # Assert issuer in response is the expected one if (not isinstance(response_ocsp.hash_algorithm, type(request_ocsp.hash_algorithm)) or response_ocsp.issuer_key_hash != request_ocsp.issuer_key_hash or response_ocsp.issuer_name_hash != request_ocsp.issuer_name_hash): raise AssertionError('the issuer does not correspond to issuer of the certificate.') # In following checks, two situations can occur: # * nextUpdate is set, and requirement is thisUpdate < now < nextUpdate # * nextUpdate is not set, and requirement is thisUpdate < now # NB1: We add a validity period tolerance to handle clock time inconsistencies, # value is 5 min like for OpenSSL. # NB2: Another check is to verify that thisUpdate is not too old, it is optional # for OpenSSL, so we do not do it here. # See OpenSSL implementation as a reference: # https://github.com/openssl/openssl/blob/ef45aa14c5af024fcb8bef1c9007f3d1c115bd85/crypto/ocsp/ocsp_cl.c#L338-L391 # thisUpdate/nextUpdate are expressed in UTC/GMT time zone now = datetime.now(pytz.UTC).replace(tzinfo=None) if not response_ocsp.this_update: raise AssertionError('param thisUpdate is not set.') if response_ocsp.this_update > now + timedelta(minutes=5): raise AssertionError('param thisUpdate is in the future.') if response_ocsp.next_update and response_ocsp.next_update < now - timedelta(minutes=5): raise AssertionError('param nextUpdate is in the past.') def _check_ocsp_response_signature(response_ocsp: 'ocsp.OCSPResponse', issuer_cert: x509.Certificate, cert_path: str) -> None: """Verify an OCSP response signature against certificate issuer or responder""" def _key_hash(cert: x509.Certificate) -> bytes: return x509.SubjectKeyIdentifier.from_public_key(cert.public_key()).digest if (response_ocsp.responder_name == issuer_cert.subject or response_ocsp.responder_key_hash == _key_hash(issuer_cert)): # Case where the OCSP responder is also the certificate issuer logger.debug('OCSP response for certificate %s is signed by the certificate\'s issuer.', cert_path) responder_cert = issuer_cert else: # Case where the OCSP responder is not the certificate issuer logger.debug('OCSP response for certificate %s is delegated to an external responder.', cert_path) responder_certs = [cert for cert in response_ocsp.certificates if response_ocsp.responder_name == cert.subject or \ response_ocsp.responder_key_hash == _key_hash(cert)] if not responder_certs: raise AssertionError('no matching responder certificate could be found') # We suppose here that the ACME server support only one certificate in the OCSP status # request. This is currently the case for LetsEncrypt servers. # See https://github.com/letsencrypt/boulder/issues/2331 responder_cert = responder_certs[0] if responder_cert.issuer != issuer_cert.subject: raise AssertionError('responder certificate is not signed ' 'by the certificate\'s issuer') try: extension = responder_cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage) delegate_authorized = x509.oid.ExtendedKeyUsageOID.OCSP_SIGNING in extension.value except (x509.ExtensionNotFound, IndexError): delegate_authorized = False if not delegate_authorized: raise AssertionError('responder is not authorized by issuer to sign OCSP responses') # Following line may raise UnsupportedAlgorithm chosen_cert_hash = responder_cert.signature_hash_algorithm assert chosen_cert_hash # always present for RSA and ECDSA certificates. # For a delegate OCSP responder, we need first check that its certificate is effectively # signed by the certificate issuer. crypto_util.verify_signed_payload(issuer_cert.public_key(), responder_cert.signature, responder_cert.tbs_certificate_bytes, chosen_cert_hash) # Following line may raise UnsupportedAlgorithm chosen_response_hash = response_ocsp.signature_hash_algorithm # We check that the OSCP response is effectively signed by the responder # (an authorized delegate one or the certificate issuer itself). if not chosen_response_hash: raise AssertionError("no signature hash algorithm defined") crypto_util.verify_signed_payload(responder_cert.public_key(), response_ocsp.signature, response_ocsp.tbs_response_bytes, chosen_response_hash) def _translate_ocsp_query(cert_path: str, ocsp_output: str, ocsp_errors: str) -> bool: """Parse openssl's weird output to work out what it means.""" states = ("good", "revoked", "unknown") patterns = [r"{0}: (WARNING.*)?{1}".format(cert_path, s) for s in states] good, revoked, unknown = (re.search(p, ocsp_output, flags=re.DOTALL) for p in patterns) warning = good.group(1) if good else None if ("Response verify OK" not in ocsp_errors) or (good and warning) or unknown: logger.info("Revocation status for %s is unknown", cert_path) logger.debug("Uncertain output:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors) return False elif good and not warning: return False elif revoked: warning = revoked.group(1) if warning: logger.info("OCSP revocation warning: %s", warning) return True else: logger.warning("Unable to properly parse OCSP output: %s\nstderr:%s", ocsp_output, ocsp_errors) return False ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3350835 certbot-2.9.0/certbot/plugins/0000775000175100017510000000000014561227516015314 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/plugins/__init__.py0000664000175100017510000000002714561227515017423 0ustar00ericaerica"""Certbot plugins.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/plugins/common.py0000664000175100017510000004216514561227515017165 0ustar00ericaerica"""Plugin common functions.""" from abc import ABCMeta from abc import abstractmethod import argparse import logging import re import shutil import sys import tempfile from typing import Any from typing import Callable from typing import Iterable from typing import List from typing import Optional from typing import Set from typing import Tuple from typing import Type from typing import TypeVar from acme import challenges from certbot import achallenges from certbot import configuration from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot import reverter from certbot._internal import constants from certbot.compat import filesystem from certbot.compat import os from certbot.interfaces import Installer as AbstractInstaller from certbot.interfaces import Plugin as AbstractPlugin from certbot.plugins.storage import PluginStorage if sys.version_info >= (3, 9): # pragma: no cover import importlib.resources as importlib_resources else: # pragma: no cover import importlib_resources logger = logging.getLogger(__name__) def option_namespace(name: str) -> str: """ArgumentParser options namespace (prefix of all options).""" return name + "-" def dest_namespace(name: str) -> str: """ArgumentParser dest namespace (prefix of all destinations).""" return name.replace("-", "_") + "_" private_ips_regex = re.compile( r"(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|" r"(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)") hostname_regex = re.compile( r"^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*[a-z]+$", re.IGNORECASE) class Plugin(AbstractPlugin, metaclass=ABCMeta): """Generic plugin.""" def __init__(self, config: configuration.NamespaceConfig, name: str) -> None: super().__init__(config, name) self.config = config self.name = name @classmethod @abstractmethod def add_parser_arguments(cls, add: Callable[..., None]) -> None: """Add plugin arguments to the CLI argument parser. :param callable add: Function that proxies calls to `argparse.ArgumentParser.add_argument` prepending options with unique plugin name prefix. """ @classmethod def inject_parser_options(cls, parser: argparse.ArgumentParser, name: str) -> None: """Inject parser options. See `~.certbot.interfaces.Plugin.inject_parser_options` for docs. """ # dummy function, doesn't check if dest.startswith(self.dest_namespace) def add(arg_name_no_prefix: str, *args: Any, **kwargs: Any) -> None: parser.add_argument( "--{0}{1}".format(option_namespace(name), arg_name_no_prefix), *args, **kwargs) return cls.add_parser_arguments(add) @property def option_namespace(self) -> str: """ArgumentParser options namespace (prefix of all options).""" return option_namespace(self.name) def option_name(self, name: str) -> str: """Option name (include plugin namespace).""" return self.option_namespace + name @property def dest_namespace(self) -> str: """ArgumentParser dest namespace (prefix of all destinations).""" return dest_namespace(self.name) def dest(self, var: str) -> str: """Find a destination for given variable ``var``.""" # this should do exactly the same what ArgumentParser(arg), # does to "arg" to compute "dest" return self.dest_namespace + var.replace("-", "_") def conf(self, var: str) -> Any: """Find a configuration value for variable ``var``.""" return getattr(self.config, self.dest(var)) def auth_hint(self, failed_achalls: List[achallenges.AnnotatedChallenge]) -> str: """Human-readable string to help the user troubleshoot the authenticator. Shown to the user if one or more of the attempted challenges were not a success. Should describe, in simple language, what the authenticator tried to do, what went wrong and what the user should try as their "next steps". TODO: auth_hint belongs in Authenticator but can't be added until the next major version of Certbot. For now, it lives in .Plugin and auth_handler will only call it on authenticators that subclass .Plugin. For now, inherit from `.Plugin` to implement and/or override the method. :param list failed_achalls: List of one or more failed challenges (:class:`achallenges.AnnotatedChallenge` subclasses). :rtype str: """ # This is a fallback hint. Authenticators should implement their own auth_hint that # addresses the specific mechanics of that authenticator. challs = " and ".join(sorted({achall.typ for achall in failed_achalls})) return ("The Certificate Authority couldn't externally verify that the {name} plugin " "completed the required {challs} challenges. Ensure the plugin is configured " "correctly and that the changes it makes are accessible from the internet." .format(name=self.name, challs=challs)) class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): """An installer base class with reverter and ssl_dhparam methods defined. Installer plugins do not have to inherit from this class. """ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.storage = PluginStorage(self.config, self.name) self.reverter = reverter.Reverter(self.config) def add_to_checkpoint(self, save_files: Set[str], save_notes: str, temporary: bool = False) -> None: """Add files to a checkpoint. :param set save_files: set of filepaths to save :param str save_notes: notes about changes during the save :param bool temporary: True if the files should be added to a temporary checkpoint rather than a permanent one. This is usually used for changes that will soon be reverted. :raises .errors.PluginError: when unable to add to checkpoint """ if temporary: checkpoint_func = self.reverter.add_to_temp_checkpoint else: checkpoint_func = self.reverter.add_to_checkpoint try: checkpoint_func(save_files, save_notes) except errors.ReverterError as err: raise errors.PluginError(str(err)) def finalize_checkpoint(self, title: str) -> None: """Timestamp and save changes made through the reverter. :param str title: Title describing checkpoint :raises .errors.PluginError: when an error occurs """ try: self.reverter.finalize_checkpoint(title) except errors.ReverterError as err: raise errors.PluginError(str(err)) def recovery_routine(self) -> None: """Revert all previously modified files. Reverts all modified files that have not been saved as a checkpoint :raises .errors.PluginError: If unable to recover the configuration """ try: self.reverter.recovery_routine() except errors.ReverterError as err: raise errors.PluginError(str(err)) def revert_temporary_config(self) -> None: """Rollback temporary checkpoint. :raises .errors.PluginError: when unable to revert config """ try: self.reverter.revert_temporary_config() except errors.ReverterError as err: raise errors.PluginError(str(err)) def rollback_checkpoints(self, rollback: int = 1) -> None: """Rollback saved checkpoints. :param int rollback: Number of checkpoints to revert :raises .errors.PluginError: If there is a problem with the input or the function is unable to correctly revert the configuration """ try: self.reverter.rollback_checkpoints(rollback) except errors.ReverterError as err: raise errors.PluginError(str(err)) @property def ssl_dhparams(self) -> str: """Full absolute path to ssl_dhparams file.""" return os.path.join(self.config.config_dir, constants.SSL_DHPARAMS_DEST) @property def updated_ssl_dhparams_digest(self) -> str: """Full absolute path to digest of updated ssl_dhparams file.""" return os.path.join(self.config.config_dir, constants.UPDATED_SSL_DHPARAMS_DIGEST) def install_ssl_dhparams(self) -> None: """Copy Certbot's ssl_dhparams file into the system's config dir if required.""" install_version_controlled_file( self.ssl_dhparams, self.updated_ssl_dhparams_digest, constants.SSL_DHPARAMS_SRC, constants.ALL_SSL_DHPARAMS_HASHES) class Configurator(Installer, interfaces.Authenticator, metaclass=ABCMeta): """ A plugin that extends certbot.plugins.common.Installer and implements certbot.interfaces.Authenticator """ GenericAddr = TypeVar("GenericAddr", bound="Addr") class Addr: r"""Represents an virtual host address. :param str addr: addr part of vhost address :param str port: port number or \*, or "" """ def __init__(self, tup: Tuple[str, str], ipv6: bool = False): self.tup = tup self.ipv6 = ipv6 @classmethod def fromstring(cls: Type[GenericAddr], str_addr: str) -> Optional[GenericAddr]: """Initialize Addr from string.""" if str_addr.startswith('['): # ipv6 addresses starts with [ endIndex = str_addr.rfind(']') host = str_addr[:endIndex + 1] port = '' if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':': port = str_addr[endIndex + 2:] return cls((host, port), ipv6=True) else: tup = str_addr.partition(':') return cls((tup[0], tup[2])) def __str__(self) -> str: if self.tup[1]: return "%s:%s" % self.tup return self.tup[0] def normalized_tuple(self) -> Tuple[str, str]: """Normalized representation of addr/port tuple """ if self.ipv6: return self.get_ipv6_exploded(), self.tup[1] return self.tup def __eq__(self, other: Any) -> bool: if isinstance(other, self.__class__): # compare normalized to take different # styles of representation into account return self.normalized_tuple() == other.normalized_tuple() return False def __hash__(self) -> int: return hash(self.tup) def get_addr(self) -> str: """Return addr part of Addr object.""" return self.tup[0] def get_port(self) -> str: """Return port.""" return self.tup[1] def get_addr_obj(self: GenericAddr, port: str) -> GenericAddr: """Return new address object with same addr and new port.""" return self.__class__((self.tup[0], port), self.ipv6) def _normalize_ipv6(self, addr: str) -> List[str]: """Return IPv6 address in normalized form, helper function""" addr = addr.lstrip("[") addr = addr.rstrip("]") return self._explode_ipv6(addr) def get_ipv6_exploded(self) -> str: """Return IPv6 in normalized form""" if self.ipv6: return ":".join(self._normalize_ipv6(self.tup[0])) return "" def _explode_ipv6(self, addr: str) -> List[str]: """Explode IPv6 address for comparison""" result = ['0', '0', '0', '0', '0', '0', '0', '0'] addr_list = addr.split(":") if len(addr_list) > len(result): # too long, truncate addr_list = addr_list[0:len(result)] append_to_end = False for i, block in enumerate(addr_list): if not block: # encountered ::, so rest of the blocks should be # appended to the end append_to_end = True continue if len(block) > 1: # remove leading zeros block = block.lstrip("0") if not append_to_end: result[i] = str(block) else: # count the location from the end using negative indices result[i-len(addr_list)] = str(block) return result class ChallengePerformer: """Abstract base for challenge performers. :ivar configurator: Authenticator and installer plugin :ivar achalls: Annotated challenges :vartype achalls: `list` of `.KeyAuthorizationAnnotatedChallenge` :ivar indices: Holds the indices of challenges from a larger array so the user of the class doesn't have to. :vartype indices: `list` of `int` """ def __init__(self, configurator: Configurator): self.configurator = configurator self.achalls: List[achallenges.KeyAuthorizationAnnotatedChallenge] = [] self.indices: List[int] = [] def add_chall(self, achall: achallenges.KeyAuthorizationAnnotatedChallenge, idx: Optional[int] = None) -> None: """Store challenge to be performed when perform() is called. :param .KeyAuthorizationAnnotatedChallenge achall: Annotated challenge. :param int idx: index to challenge in a larger array """ self.achalls.append(achall) if idx is not None: self.indices.append(idx) def perform(self) -> List[challenges.KeyAuthorizationChallengeResponse]: """Perform all added challenges. :returns: challenge responses :rtype: `list` of `acme.challenges.KeyAuthorizationChallengeResponse` """ raise NotImplementedError() def install_version_controlled_file(dest_path: str, digest_path: str, src_path: str, all_hashes: Iterable[str]) -> None: """Copy a file into an active location (likely the system's config dir) if required. :param str dest_path: destination path for version controlled file :param str digest_path: path to save a digest of the file in :param str src_path: path to version controlled file found in distribution :param list all_hashes: hashes of every released version of the file """ current_hash = crypto_util.sha256sum(src_path) def _write_current_hash() -> None: with open(digest_path, "w") as file_h: file_h.write(current_hash) def _install_current_file() -> None: shutil.copyfile(src_path, dest_path) _write_current_hash() # Check to make sure options-ssl.conf is installed if not os.path.isfile(dest_path): _install_current_file() return # there's already a file there. if it's up to date, do nothing. if it's not but # it matches a known file hash, we can update it. # otherwise, print a warning once per new version. active_file_digest = crypto_util.sha256sum(dest_path) if active_file_digest == current_hash: # already up to date return if active_file_digest in all_hashes: # safe to update _install_current_file() else: # has been manually modified, not safe to update # did they modify the current version or an old version? if os.path.isfile(digest_path): with open(digest_path, "r") as f: saved_digest = f.read() # they modified it after we either installed or told them about this version, so return if saved_digest == current_hash: return # there's a new version but we couldn't update the file, or they deleted the digest. # save the current digest so we only print this once, and print a warning _write_current_hash() logger.warning("%s has been manually modified; updated file " "saved to %s. We recommend updating %s for security purposes.", dest_path, src_path, dest_path) # test utils used by certbot_apache/certbot_nginx (hence # "pragma: no cover") TODO: this might quickly lead to dead code (also # c.f. #383) def dir_setup(test_dir: str, pkg: str) -> Tuple[str, str, str]: # pragma: no cover """Setup the directories necessary for the configurator.""" def expanded_tempdir(prefix: str) -> str: """Return the real path of a temp directory with the specified prefix Some plugins rely on real paths of symlinks for working correctly. For example, certbot-apache uses real paths of configuration files to tell a virtual host from another. On systems where TMP itself is a symbolic link, (ex: OS X) such plugins will be confused. This function prevents such a case. """ return filesystem.realpath(tempfile.mkdtemp(prefix)) temp_dir = expanded_tempdir("temp") config_dir = expanded_tempdir("config") work_dir = expanded_tempdir("work") filesystem.chmod(temp_dir, constants.CONFIG_DIRS_MODE) filesystem.chmod(config_dir, constants.CONFIG_DIRS_MODE) filesystem.chmod(work_dir, constants.CONFIG_DIRS_MODE) test_dir_ref = importlib_resources.files(pkg).joinpath("testdata").joinpath(test_dir) with importlib_resources.as_file(test_dir_ref) as path: shutil.copytree( path, os.path.join(temp_dir, test_dir), symlinks=True) return temp_dir, config_dir, work_dir ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/plugins/dns_common.py0000664000175100017510000003331414561227515020025 0ustar00ericaerica"""Common code for DNS Authenticator Plugins.""" import abc import logging from time import sleep from typing import Callable from typing import Iterable from typing import List from typing import Mapping from typing import Optional from typing import Type import configobj from acme import challenges from certbot import achallenges from certbot import configuration from certbot import errors from certbot import interfaces from certbot.compat import filesystem from certbot.compat import os from certbot.display import ops from certbot.display import util as display_util from certbot.plugins import common logger = logging.getLogger(__name__) class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.ABCMeta): """Base class for DNS Authenticators""" def __init__(self, config: configuration.NamespaceConfig, name: str) -> None: super().__init__(config, name) self._attempt_cleanup = False @classmethod def add_parser_arguments(cls, add: Callable[..., None], # pylint: disable=arguments-differ default_propagation_seconds: int = 10) -> None: add('propagation-seconds', default=default_propagation_seconds, type=int, help='The number of seconds to wait for DNS to propagate before asking the ACME server ' 'to verify the DNS record.') def auth_hint(self, failed_achalls: List[achallenges.AnnotatedChallenge]) -> str: """See certbot.plugins.common.Plugin.auth_hint.""" delay = self.conf('propagation-seconds') return ( 'The Certificate Authority failed to verify the DNS TXT records created by --{name}. ' 'Ensure the above domains are hosted by this DNS provider, or try increasing ' '--{name}-propagation-seconds (currently {secs} second{suffix}).' .format(name=self.name, secs=delay, suffix='s' if delay != 1 else '') ) def get_chall_pref(self, unused_domain: str) -> Iterable[Type[challenges.Challenge]]: # pylint: disable=missing-function-docstring return [challenges.DNS01] def prepare(self) -> None: # pylint: disable=missing-function-docstring pass def more_info(self) -> str: # pylint: disable=missing-function-docstring raise NotImplementedError() def perform(self, achalls: List[achallenges.AnnotatedChallenge] ) -> List[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring self._setup_credentials() self._attempt_cleanup = True responses = [] for achall in achalls: domain = achall.domain validation_domain_name = achall.validation_domain_name(domain) validation = achall.validation(achall.account_key) self._perform(domain, validation_domain_name, validation) responses.append(achall.response(achall.account_key)) # DNS updates take time to propagate and checking to see if the update has occurred is not # reliable (the machine this code is running on might be able to see an update before # the ACME server). So: we sleep for a short amount of time we believe to be long enough. display_util.notify("Waiting %d seconds for DNS changes to propagate" % self.conf('propagation-seconds')) sleep(self.conf('propagation-seconds')) return responses def cleanup(self, achalls: List[achallenges.AnnotatedChallenge]) -> None: # pylint: disable=missing-function-docstring if self._attempt_cleanup: for achall in achalls: domain = achall.domain validation_domain_name = achall.validation_domain_name(domain) validation = achall.validation(achall.account_key) self._cleanup(domain, validation_domain_name, validation) @abc.abstractmethod def _setup_credentials(self) -> None: # pragma: no cover """ Establish credentials, prompting if necessary. """ raise NotImplementedError() @abc.abstractmethod def _perform(self, domain: str, validation_name: str, validation: str) -> None: # pragma: no cover """ Performs a dns-01 challenge by creating a DNS TXT record. :param str domain: The domain being validated. :param str validation_domain_name: The validation record domain name. :param str validation: The validation record content. :raises errors.PluginError: If the challenge cannot be performed """ raise NotImplementedError() @abc.abstractmethod def _cleanup(self, domain: str, validation_name: str, validation: str) -> None: # pragma: no cover """ Deletes the DNS TXT record which would have been created by `_perform_achall`. Fails gracefully if no such record exists. :param str domain: The domain being validated. :param str validation_domain_name: The validation record domain name. :param str validation: The validation record content. """ raise NotImplementedError() def _configure(self, key: str, label: str) -> None: """ Ensure that a configuration value is available. If necessary, prompts the user and stores the result. :param str key: The configuration key. :param str label: The user-friendly label for this piece of information. """ configured_value = self.conf(key) if not configured_value: new_value = self._prompt_for_data(label) setattr(self.config, self.dest(key), new_value) def _configure_file(self, key: str, label: str, validator: Optional[Callable[[str], None]] = None) -> None: """ Ensure that a configuration value is available for a path. If necessary, prompts the user and stores the result. :param str key: The configuration key. :param str label: The user-friendly label for this piece of information. """ configured_value = self.conf(key) if not configured_value: new_value = self._prompt_for_file(label, validator) setattr(self.config, self.dest(key), os.path.abspath(os.path.expanduser(new_value))) def _configure_credentials( self, key: str, label: str, required_variables: Optional[Mapping[str, str]] = None, validator: Optional[Callable[['CredentialsConfiguration'], None]] = None ) -> 'CredentialsConfiguration': """ As `_configure_file`, but for a credential configuration file. If necessary, prompts the user and stores the result. Always stores absolute paths to avoid issues during renewal. :param str key: The configuration key. :param str label: The user-friendly label for this piece of information. :param dict required_variables: Map of variable which must be present to error to display. :param callable validator: A method which will be called to validate the `CredentialsConfiguration` resulting from the supplied input after it has been validated to contain the `required_variables`. Should throw a `~certbot.errors.PluginError` to indicate any issue. """ def __validator(filename: str) -> None: # pylint: disable=unused-private-member applied_configuration = CredentialsConfiguration(filename, self.dest) if required_variables: applied_configuration.require(required_variables) if validator: validator(applied_configuration) self._configure_file(key, label, __validator) credentials_configuration = CredentialsConfiguration(self.conf(key), self.dest) if required_variables: credentials_configuration.require(required_variables) if validator: validator(credentials_configuration) return credentials_configuration @staticmethod def _prompt_for_data(label: str) -> str: """ Prompt the user for a piece of information. :param str label: The user-friendly label for this piece of information. :returns: The user's response (guaranteed non-empty). :rtype: str """ def __validator(i: str) -> None: # pylint: disable=unused-private-member if not i: raise errors.PluginError('Please enter your {0}.'.format(label)) code, response = ops.validated_input( __validator, 'Input your {0}'.format(label), force_interactive=True) if code == display_util.OK: return response raise errors.PluginError('{0} required to proceed.'.format(label)) @staticmethod def _prompt_for_file(label: str, validator: Optional[Callable[[str], None]] = None) -> str: """ Prompt the user for a path. :param str label: The user-friendly label for the file. :param callable validator: A method which will be called to validate the supplied input after it has been validated to be a non-empty path to an existing file. Should throw a `~certbot.errors.PluginError` to indicate any issue. :returns: The user's response (guaranteed to exist). :rtype: str """ def __validator(filename: str) -> None: # pylint: disable=unused-private-member if not filename: raise errors.PluginError('Please enter a valid path to your {0}.'.format(label)) filename = os.path.expanduser(filename) validate_file(filename) if validator: validator(filename) code, response = ops.validated_directory( __validator, 'Input the path to your {0}'.format(label), force_interactive=True) if code == display_util.OK: return response raise errors.PluginError('{0} required to proceed.'.format(label)) class CredentialsConfiguration: """Represents a user-supplied filed which stores API credentials.""" def __init__(self, filename: str, mapper: Callable[[str], str] = lambda x: x) -> None: """ :param str filename: A path to the configuration file. :param callable mapper: A transformation to apply to configuration key names :raises errors.PluginError: If the file does not exist or is not a valid format. """ validate_file_permissions(filename) try: self.confobj = configobj.ConfigObj(filename) except configobj.ConfigObjError as e: logger.debug( "Error parsing credentials configuration '%s': %s", filename, e, exc_info=True ) raise errors.PluginError( "Error parsing credentials configuration '{}': {}".format( filename, e ) ) self.mapper = mapper def require(self, required_variables: Mapping[str, str]) -> None: """Ensures that the supplied set of variables are all present in the file. :param dict required_variables: Map of variable which must be present to error to display. :raises errors.PluginError: If one or more are missing. """ messages = [] for var in required_variables: if not self._has(var): messages.append('Property "{0}" not found (should be {1}).' .format(self.mapper(var), required_variables[var])) elif not self._get(var): messages.append('Property "{0}" not set (should be {1}).' .format(self.mapper(var), required_variables[var])) if messages: raise errors.PluginError( 'Missing {0} in credentials configuration file {1}:\n * {2}'.format( 'property' if len(messages) == 1 else 'properties', self.confobj.filename, '\n * '.join(messages) ) ) def conf(self, var: str) -> Optional[str]: """Find a configuration value for variable `var`, as transformed by `mapper`. :param str var: The variable to get. :returns: The value of the variable, if it exists. :rtype: str or None """ return self._get(var) def _has(self, var: str) -> bool: return self.mapper(var) in self.confobj def _get(self, var: str) -> Optional[str]: return self.confobj.get(self.mapper(var)) def validate_file(filename: str) -> None: """Ensure that the specified file exists.""" if not os.path.exists(filename): raise errors.PluginError('File not found: {0}'.format(filename)) if os.path.isdir(filename): raise errors.PluginError('Path is a directory: {0}'.format(filename)) def validate_file_permissions(filename: str) -> None: """Ensure that the specified file exists and warn about unsafe permissions.""" validate_file(filename) if filesystem.has_world_permissions(filename): logger.warning('Unsafe permissions on credentials configuration file: %s', filename) def base_domain_name_guesses(domain: str) -> List[str]: """Return a list of progressively less-specific domain names. One of these will probably be the domain name known to the DNS provider. :Example: >>> base_domain_name_guesses('foo.bar.baz.example.com') ['foo.bar.baz.example.com', 'bar.baz.example.com', 'baz.example.com', 'example.com', 'com'] :param str domain: The domain for which to return guesses. :returns: The a list of less specific domain names. :rtype: list """ fragments = domain.split('.') return ['.'.join(fragments[i:]) for i in range(0, len(fragments))] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/plugins/dns_common_lexicon.py0000664000175100017510000003123214561227515021543 0ustar00ericaerica"""Common code for DNS Authenticator Plugins built on Lexicon.""" import abc import logging import sys from types import ModuleType from typing import Any from typing import cast from typing import Dict from typing import List from typing import Mapping from typing import Optional from typing import Tuple from typing import Union import warnings from requests.exceptions import HTTPError from requests.exceptions import RequestException from certbot import configuration from certbot import errors from certbot.plugins import dns_common # Lexicon is not declared as a dependency in Certbot itself, # but in the Certbot plugins backed by Lexicon. # So we catch import error here to allow this module to be # always importable, even if it does not make sense to use it # if Lexicon is not available, obviously. try: from lexicon.client import Client from lexicon.config import ConfigResolver from lexicon.interfaces import Provider except ImportError: # pragma: no cover Client = None # type: ignore ConfigResolver = None # type: ignore Provider = None # type: ignore logger = logging.getLogger(__name__) class LexiconClient: # pragma: no cover """ Encapsulates all communication with a DNS provider via Lexicon. .. deprecated:: 2.7.0 Please use certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator instead. """ def __init__(self) -> None: self.provider: Provider def add_txt_record(self, domain: str, record_name: str, record_content: str) -> None: """ Add a TXT record using the supplied information. :param str domain: The domain to use to look up the managed zone. :param str record_name: The record name (typically beginning with '_acme-challenge.'). :param str record_content: The record content (typically the challenge validation). :raises errors.PluginError: if an error occurs communicating with the DNS Provider API """ self._find_domain_id(domain) try: self.provider.create_record(rtype='TXT', name=record_name, content=record_content) except RequestException as e: logger.debug('Encountered error adding TXT record: %s', e, exc_info=True) raise errors.PluginError('Error adding TXT record: {0}'.format(e)) def del_txt_record(self, domain: str, record_name: str, record_content: str) -> None: """ Delete a TXT record using the supplied information. :param str domain: The domain to use to look up the managed zone. :param str record_name: The record name (typically beginning with '_acme-challenge.'). :param str record_content: The record content (typically the challenge validation). :raises errors.PluginError: if an error occurs communicating with the DNS Provider API """ try: self._find_domain_id(domain) except errors.PluginError as e: logger.debug('Encountered error finding domain_id during deletion: %s', e, exc_info=True) return try: self.provider.delete_record(rtype='TXT', name=record_name, content=record_content) except RequestException as e: logger.debug('Encountered error deleting TXT record: %s', e, exc_info=True) def _find_domain_id(self, domain: str) -> None: """ Find the domain_id for a given domain. :param str domain: The domain for which to find the domain_id. :raises errors.PluginError: if the domain_id cannot be found. """ domain_name_guesses = dns_common.base_domain_name_guesses(domain) for domain_name in domain_name_guesses: try: if hasattr(self.provider, 'options'): # For Lexicon 2.x self.provider.options['domain'] = domain_name else: # For Lexicon 3.x self.provider.domain = domain_name self.provider.authenticate() return # If `authenticate` doesn't throw an exception, we've found the right name except HTTPError as e: result1 = self._handle_http_error(e, domain_name) if result1: raise result1 except Exception as e: # pylint: disable=broad-except result2 = self._handle_general_error(e, domain_name) if result2: raise result2 # pylint: disable=raising-bad-type raise errors.PluginError('Unable to determine zone identifier for {0} using zone names: {1}' .format(domain, domain_name_guesses)) def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]: return errors.PluginError('Error determining zone identifier for {0}: {1}.' .format(domain_name, e)) def _handle_general_error(self, e: Exception, domain_name: str) -> Optional[errors.PluginError]: if not str(e).startswith('No domain found'): return errors.PluginError('Unexpected error determining zone identifier for {0}: {1}' .format(domain_name, e)) return None def build_lexicon_config(lexicon_provider_name: str, lexicon_options: Mapping[str, Any], provider_options: Mapping[str, Any] ) -> Union[ConfigResolver, Dict[str, Any]]: # pragma: no cover """ Convenient function to build a Lexicon 2.x/3.x config object. :param str lexicon_provider_name: the name of the lexicon provider to use :param dict lexicon_options: options specific to lexicon :param dict provider_options: options specific to provider :return: configuration to apply to the provider :rtype: ConfigurationResolver or dict .. deprecated:: 2.7.0 Please use certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator instead. """ config_dict: Dict[str, Any] = {'provider_name': lexicon_provider_name} config_dict.update(lexicon_options) if ConfigResolver is None: # Lexicon 2.x config_dict.update(provider_options) return config_dict else: # Lexicon 3.x provider_config: Dict[str, Any] = {} provider_config.update(provider_options) config_dict[lexicon_provider_name] = provider_config return ConfigResolver().with_dict(config_dict).with_env() class LexiconDNSAuthenticator(dns_common.DNSAuthenticator): """ Base class for a DNS authenticator that uses Lexicon client as backend to execute DNS record updates """ def __init__(self, config: configuration.NamespaceConfig, name: str): super().__init__(config, name) self._provider_options: List[Tuple[str, str, str]] = [] self._credentials: dns_common.CredentialsConfiguration @property @abc.abstractmethod def _provider_name(self) -> str: """ The name of the Lexicon provider to use """ @property def _ttl(self) -> int: """ Time to live to apply to the DNS records created by this Authenticator """ return 60 def _add_provider_option(self, creds_var_name: str, creds_var_label: str, lexicon_provider_option_name: str) -> None: self._provider_options.append( (creds_var_name, creds_var_label, lexicon_provider_option_name)) def _build_lexicon_config(self, domain: str) -> ConfigResolver: if not hasattr(self, '_credentials'): # pragma: no cover self._setup_credentials() dict_config = { 'domain': domain, # We bypass Lexicon subdomain resolution by setting the 'delegated' field in the config # to the value of the 'domain' field itself. Here we consider that the domain passed to # _build_lexicon_config() is already the exact subdomain of the actual DNS zone to use. 'delegated': domain, 'provider_name': self._provider_name, 'ttl': self._ttl, self._provider_name: {item[2]: self._credentials.conf(item[0]) for item in self._provider_options} } return ConfigResolver().with_dict(dict_config).with_env() def _setup_credentials(self) -> None: self._credentials = self._configure_credentials( key='credentials', label=f'Credentials INI file for {self._provider_name} DNS authenticator', required_variables={item[0]: item[1] for item in self._provider_options}, ) def _perform(self, domain: str, validation_name: str, validation: str) -> None: resolved_domain = self._resolve_domain(domain) try: with Client(self._build_lexicon_config(resolved_domain)) as operations: operations.create_record(rtype='TXT', name=validation_name, content=validation) except RequestException as e: logger.debug('Encountered error adding TXT record: %s', e, exc_info=True) raise errors.PluginError('Error adding TXT record: {0}'.format(e)) def _cleanup(self, domain: str, validation_name: str, validation: str) -> None: try: resolved_domain = self._resolve_domain(domain) except errors.PluginError as e: logger.debug('Encountered error finding domain_id during deletion: %s', e, exc_info=True) return try: with Client(self._build_lexicon_config(resolved_domain)) as operations: operations.delete_record(rtype='TXT', name=validation_name, content=validation) except RequestException as e: logger.debug('Encountered error deleting TXT record: %s', e, exc_info=True) def _resolve_domain(self, domain: str) -> str: domain_name_guesses = dns_common.base_domain_name_guesses(domain) for domain_name in domain_name_guesses: try: # Using client as a context manager requires `dns-lexicon>=3.14` and we may want to # provide better checks and error handling around this in the future. with Client(self._build_lexicon_config(domain_name)): return domain_name except HTTPError as e: result1 = self._handle_http_error(e, domain_name) if result1: raise result1 except Exception as e: # pylint: disable=broad-except result2 = self._handle_general_error(e, domain_name) if result2: raise result2 # pylint: disable=raising-bad-type raise errors.PluginError('Unable to determine zone identifier for {0} using zone names: {1}' .format(domain, domain_name_guesses)) def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]: return errors.PluginError('Error determining zone identifier for {0}: {1}.' .format(domain_name, e)) def _handle_general_error(self, e: Exception, domain_name: str) -> Optional[errors.PluginError]: if not str(e).startswith('No domain found'): return errors.PluginError('Unexpected error determining zone identifier for {0}: {1}' .format(domain_name, e)) return None # This class takes a similar approach to the cryptography project to deprecate attributes # in public modules. See the _ModuleWithDeprecation class here: # https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 class _DeprecationModule: """ Internal class delegating to a module, and displaying warnings when attributes related to deprecated attributes in the current module. """ def __init__(self, module: ModuleType): self.__dict__['_module'] = module def __getattr__(self, attr: str) -> Any: if attr in ('LexiconClient', 'build_lexicon_config'): warnings.warn(f'{attr} attribute in {__name__} module is deprecated ' 'and will be removed soon.', DeprecationWarning, stacklevel=2) return getattr(self._module, attr) def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover setattr(self._module, attr, value) def __delattr__(self, attr: str) -> Any: # pragma: no cover delattr(self._module, attr) def __dir__(self) -> List[str]: # pragma: no cover return ['_module'] + dir(self._module) # Patching ourselves to warn about deprecation and planned removal of some elements in the module. sys.modules[__name__] = cast(ModuleType, _DeprecationModule(sys.modules[__name__])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/plugins/dns_test_common.py0000664000175100017510000000511314561227515021060 0ustar00ericaerica"""Base test class for DNS authenticators.""" from typing import Any from typing import Mapping from typing import Protocol from unittest import mock import configobj import josepy as jose from acme import challenges from certbot import achallenges from certbot.compat import filesystem from certbot.plugins.dns_common import DNSAuthenticator from certbot.tests import acme_util from certbot.tests import util as test_util DOMAIN = 'example.com' KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class _AuthenticatorCallableTestCase(Protocol): """Protocol describing a TestCase able to call a real DNSAuthenticator instance.""" auth: DNSAuthenticator def assertTrue(self, *unused_args: Any) -> None: """ See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertTrue """ def assertEqual(self, *unused_args: Any) -> None: """ See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual """ def assertRaises(self, *unused_args: Any) -> None: """ See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises """ class BaseAuthenticatorTest: """ A base test class to reduce duplication between test code for DNS Authenticator Plugins. Assumes: * That subclasses also subclass unittest.TestCase * That the authenticator is stored as self.auth """ achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.DNS01, domain=DOMAIN, account_key=KEY) def test_more_info(self: _AuthenticatorCallableTestCase) -> None: self.assertTrue(isinstance(self.auth.more_info(), str)) # pylint: disable=no-member def test_get_chall_pref(self: _AuthenticatorCallableTestCase) -> None: self.assertEqual(self.auth.get_chall_pref("example.org"), [challenges.DNS01]) # pylint: disable=no-member def test_parser_arguments(self: _AuthenticatorCallableTestCase) -> None: m = mock.MagicMock() self.auth.add_parser_arguments(m) # pylint: disable=no-member m.assert_any_call('propagation-seconds', type=int, default=mock.ANY, help=mock.ANY) def write(values: Mapping[str, Any], path: str) -> None: """Write the specified values to a config file. :param dict values: A map of values to write. :param str path: Where to write the values. """ config = configobj.ConfigObj() for key in values: config[key] = values[key] with open(path, "wb") as f: config.write(outfile=f) filesystem.chmod(path, 0o600) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/plugins/dns_test_common_lexicon.py0000664000175100017510000003654414561227515022615 0ustar00ericaerica"""Base test class for DNS authenticators built on Lexicon.""" import contextlib import sys from types import ModuleType from typing import Any from typing import cast from typing import Generator from typing import List from typing import Protocol from typing import Tuple from unittest import mock from unittest.mock import MagicMock import warnings import josepy as jose from requests import Response from requests.exceptions import HTTPError from requests.exceptions import RequestException from certbot import errors from certbot.achallenges import AnnotatedChallenge from certbot.plugins import dns_test_common with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) from certbot.plugins.dns_common_lexicon import LexiconClient from certbot.plugins.dns_test_common import _AuthenticatorCallableTestCase from certbot.tests import util as test_util DOMAIN = 'example.com' KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) DOMAIN_NOT_FOUND = Exception('No domain found') GENERIC_ERROR = RequestException LOGIN_ERROR = HTTPError('400 Client Error: ...', response=Response()) UNKNOWN_LOGIN_ERROR = HTTPError('500 Surprise! Error: ...', response=Response()) class _AuthenticatorCallableLexiconTestCase(_AuthenticatorCallableTestCase, Protocol): """ Protocol describing a TestCase suitable to test challenges against a mocked LexiconClient instance. """ mock_client: MagicMock achall: AnnotatedChallenge class _LexiconAwareTestCase(Protocol): """ Protocol describing a TestCase suitable to test a real LexiconClient instance. """ client: LexiconClient provider_mock: MagicMock record_prefix: str record_name: str record_content: str DOMAIN_NOT_FOUND: Exception GENERIC_ERROR: Exception LOGIN_ERROR: Exception UNKNOWN_LOGIN_ERROR: Exception def assertRaises(self, *unused_args: Any) -> None: """ See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises """ # These classes are intended to be subclassed/mixed in, so not all members are defined. # pylint: disable=no-member class BaseLexiconAuthenticatorTest(dns_test_common.BaseAuthenticatorTest): # pragma: no cover @test_util.patch_display_util() def test_perform(self: _AuthenticatorCallableLexiconTestCase, unused_mock_get_utility: Any) -> None: self.auth.perform([self.achall]) expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] self.assertEqual(expected, self.mock_client.mock_calls) def test_cleanup(self: _AuthenticatorCallableLexiconTestCase) -> None: self.auth._attempt_cleanup = True # _attempt_cleanup | pylint: disable=protected-access self.auth.cleanup([self.achall]) expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] self.assertEqual(expected, self.mock_client.mock_calls) class BaseLexiconClientTest: # pragma: no cover DOMAIN_NOT_FOUND = DOMAIN_NOT_FOUND GENERIC_ERROR = GENERIC_ERROR LOGIN_ERROR = LOGIN_ERROR UNKNOWN_LOGIN_ERROR = UNKNOWN_LOGIN_ERROR record_prefix = "_acme-challenge" record_name = record_prefix + "." + DOMAIN record_content = "bar" def test_add_txt_record(self: _LexiconAwareTestCase) -> None: self.client.add_txt_record(DOMAIN, self.record_name, self.record_content) self.provider_mock.create_record.assert_called_with(rtype='TXT', name=self.record_name, content=self.record_content) def test_add_txt_record_try_twice_to_find_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = [self.DOMAIN_NOT_FOUND, ''] self.client.add_txt_record(DOMAIN, self.record_name, self.record_content) self.provider_mock.create_record.assert_called_with(rtype='TXT', name=self.record_name, content=self.record_content) def test_add_txt_record_fail_to_find_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = [self.DOMAIN_NOT_FOUND, self.DOMAIN_NOT_FOUND, self.DOMAIN_NOT_FOUND,] self.assertRaises(errors.PluginError, self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) def test_add_txt_record_fail_to_authenticate(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.LOGIN_ERROR self.assertRaises(errors.PluginError, self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) def test_add_txt_record_fail_to_authenticate_with_unknown_error( self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.UNKNOWN_LOGIN_ERROR self.assertRaises(errors.PluginError, self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) def test_add_txt_record_error_finding_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.GENERIC_ERROR self.assertRaises(errors.PluginError, self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) def test_add_txt_record_error_adding_record(self: _LexiconAwareTestCase) -> None: self.provider_mock.create_record.side_effect = self.GENERIC_ERROR self.assertRaises(errors.PluginError, self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) def test_del_txt_record(self: _LexiconAwareTestCase) -> None: self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) self.provider_mock.delete_record.assert_called_with(rtype='TXT', name=self.record_name, content=self.record_content) def test_del_txt_record_fail_to_find_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = [self.DOMAIN_NOT_FOUND, self.DOMAIN_NOT_FOUND, self.DOMAIN_NOT_FOUND, ] self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) def test_del_txt_record_fail_to_authenticate(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.LOGIN_ERROR self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) def test_del_txt_record_fail_to_authenticate_with_unknown_error( self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.UNKNOWN_LOGIN_ERROR self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) def test_del_txt_record_error_finding_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.GENERIC_ERROR self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) def test_del_txt_record_error_deleting_record(self: _LexiconAwareTestCase) -> None: self.provider_mock.delete_record.side_effect = self.GENERIC_ERROR self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) class _BaseLexiconDNSAuthenticatorTestProto(_AuthenticatorCallableTestCase, Protocol): """Protocol for BaseLexiconDNSAuthenticatorTest instances""" DOMAIN_NOT_FOUND: Exception GENERIC_ERROR: Exception LOGIN_ERROR: Exception UNKNOWN_LOGIN_ERROR: Exception achall: AnnotatedChallenge class BaseLexiconDNSAuthenticatorTest(dns_test_common.BaseAuthenticatorTest): DOMAIN_NOT_FOUND = DOMAIN_NOT_FOUND GENERIC_ERROR = GENERIC_ERROR LOGIN_ERROR = LOGIN_ERROR UNKNOWN_LOGIN_ERROR = UNKNOWN_LOGIN_ERROR def test_perform_succeed(self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with test_util.patch_display_util(): with _patch_lexicon_client() as (mock_client, mock_operations): self.auth.perform([self.achall]) mock_client.assert_called() config = mock_client.call_args[0][0] self.assertEqual(DOMAIN, config.resolve('lexicon:domain')) mock_operations.create_record.assert_called_with( rtype='TXT', name=f'_acme-challenge.{DOMAIN}', content=mock.ANY) def test_perform_with_one_domain_resolution_failure_succeed( self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with test_util.patch_display_util(): with _patch_lexicon_client() as (mock_client, mock_operations): mock_client.return_value.__enter__.side_effect = [ self.DOMAIN_NOT_FOUND, # First resolution domain attempt mock_operations, # Second resolution domain attempt mock_operations, # Create record operation ] self.auth.perform([self.achall]) def test_perform_with_two_domain_resolution_failures_raise( self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with test_util.patch_display_util(): with _patch_lexicon_client() as (mock_client, _): mock_client.return_value.__enter__.side_effect = self.DOMAIN_NOT_FOUND self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) def test_perform_with_domain_resolution_general_failure_raise( self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with test_util.patch_display_util(): with _patch_lexicon_client() as (mock_client, _): mock_client.return_value.__enter__.side_effect = self.GENERIC_ERROR self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) def test_perform_with_auth_failure_raise(self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with test_util.patch_display_util(): with _patch_lexicon_client() as (mock_client, _): mock_client.side_effect = self.LOGIN_ERROR self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) def test_perform_with_unknown_auth_failure_raise( self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with test_util.patch_display_util(): with _patch_lexicon_client() as (mock_client, _): mock_client.side_effect = self.UNKNOWN_LOGIN_ERROR self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) def test_perform_with_create_record_failure_raise( self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with test_util.patch_display_util(): with _patch_lexicon_client() as (_, mock_operations): mock_operations.create_record.side_effect = self.GENERIC_ERROR self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) def test_cleanup_success(self: _BaseLexiconDNSAuthenticatorTestProto) -> None: self.auth._attempt_cleanup = True # _attempt_cleanup | pylint: disable=protected-access with _patch_lexicon_client() as (mock_client, mock_operations): self.auth.cleanup([self.achall]) mock_client.assert_called() config = mock_client.call_args[0][0] self.assertEqual(DOMAIN, config.resolve('lexicon:domain')) mock_operations.delete_record.assert_called_with( rtype='TXT', name=f'_acme-challenge.{DOMAIN}', content=mock.ANY) def test_cleanup_with_auth_failure_ignore(self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with _patch_lexicon_client() as (mock_client, _): mock_client.side_effect = self.LOGIN_ERROR self.auth.cleanup([self.achall]) def test_cleanup_with_unknown_auth_failure_ignore( self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with _patch_lexicon_client() as (mock_client, _): mock_client.side_effect = self.LOGIN_ERROR self.auth.cleanup([self.achall]) def test_cleanup_with_domain_resolution_failure_ignore( self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with _patch_lexicon_client() as (mock_client, _): mock_client.return_value.__enter__.side_effect = self.DOMAIN_NOT_FOUND self.auth.cleanup([self.achall]) def test_cleanup_with_domain_resolution_general_failure_ignore( self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with _patch_lexicon_client() as (mock_client, _): mock_client.return_value.__enter__.side_effect = self.GENERIC_ERROR self.auth.cleanup([self.achall]) def test_cleanup_with_delete_record_failure_ignore( self: _BaseLexiconDNSAuthenticatorTestProto) -> None: with _patch_lexicon_client() as (_, mock_operations): mock_operations.create_record.side_effect = self.GENERIC_ERROR self.auth.cleanup([self.achall]) @contextlib.contextmanager def _patch_lexicon_client() -> Generator[Tuple[MagicMock, MagicMock], None, None]: with mock.patch('certbot.plugins.dns_common_lexicon.Client') as mock_client: mock_operations = MagicMock() mock_client.return_value.__enter__.return_value = mock_operations yield mock_client, mock_operations # This class takes a similar approach to the cryptography project to deprecate attributes # in public modules. See the _ModuleWithDeprecation class here: # https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 class _DeprecationModule: """ Internal class delegating to a module, and displaying warnings when attributes related to deprecated attributes in the current module. """ def __init__(self, module: ModuleType): self.__dict__['_module'] = module def __getattr__(self, attr: str) -> Any: if attr in ('BaseLexiconAuthenticatorTest', 'BaseLexiconClientTest'): warnings.warn(f'{attr} attribute in {__name__} module is deprecated ' 'and will be removed soon.', DeprecationWarning, stacklevel=2) return getattr(self._module, attr) def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover setattr(self._module, attr, value) def __delattr__(self, attr: str) -> Any: # pragma: no cover delattr(self._module, attr) def __dir__(self) -> List[str]: # pragma: no cover return ['_module'] + dir(self._module) # Patching ourselves to warn about deprecation and planned removal of some elements in the module. sys.modules[__name__] = cast(ModuleType, _DeprecationModule(sys.modules[__name__])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/plugins/enhancements.py0000664000175100017510000001523114561227515020337 0ustar00ericaerica"""New interface style Certbot enhancements""" import abc from typing import Any from typing import Callable from typing import Dict from typing import Generator from typing import Iterable from typing import List from typing import Optional from certbot import configuration from certbot import interfaces from certbot._internal import constants ENHANCEMENTS = ["redirect", "ensure-http-header", "ocsp-stapling"] """List of possible :class:`certbot.interfaces.Installer` enhancements. List of expected options parameters: - redirect: None - ensure-http-header: name of header (i.e. Strict-Transport-Security) - ocsp-stapling: certificate chain file path """ def enabled_enhancements( config: configuration.NamespaceConfig) -> Generator[Dict[str, Any], None, None]: """ Generator to yield the enabled new style enhancements. :param config: Configuration. :type config: certbot.configuration.NamespaceConfig """ for enh in _INDEX: if getattr(config, enh["cli_dest"]): yield enh def are_requested(config: configuration.NamespaceConfig) -> bool: """ Checks if one or more of the requested enhancements are those of the new enhancement interfaces. :param config: Configuration. :type config: certbot.configuration.NamespaceConfig """ return any(enabled_enhancements(config)) def are_supported(config: configuration.NamespaceConfig, installer: Optional[interfaces.Installer]) -> bool: """ Checks that all of the requested enhancements are supported by the installer. :param config: Configuration. :type config: certbot.configuration.NamespaceConfig :param installer: Installer object :type installer: interfaces.Installer :returns: If all the requested enhancements are supported by the installer :rtype: bool """ for enh in enabled_enhancements(config): if not isinstance(installer, enh["class"]): return False return True def enable(lineage: Optional[interfaces.RenewableCert], domains: Iterable[str], installer: Optional[interfaces.Installer], config: configuration.NamespaceConfig) -> None: """ Run enable method for each requested enhancement that is supported. :param lineage: Certificate lineage object :type lineage: certbot.interfaces.RenewableCert :param domains: List of domains in certificate to enhance :type domains: str :param installer: Installer object :type installer: interfaces.Installer :param config: Configuration. :type config: certbot.configuration.NamespaceConfig """ if installer: for enh in enabled_enhancements(config): getattr(installer, enh["enable_function"])(lineage, domains) def populate_cli(add: Callable[..., None]) -> None: """ Populates the command line flags for certbot._internal.cli.HelpfulParser :param add: Add function of certbot._internal.cli.HelpfulParser :type add: func """ for enh in _INDEX: add(enh["cli_groups"], enh["cli_flag"], action=enh["cli_action"], dest=enh["cli_dest"], default=enh["cli_flag_default"], help=enh["cli_help"]) class AutoHSTSEnhancement(object, metaclass=abc.ABCMeta): """ Enhancement interface that installer plugins can implement in order to provide functionality that configures the software to have a 'Strict-Transport-Security' with initially low max-age value that will increase over time. The plugins implementing new style enhancements are responsible of handling the saving of configuration checkpoints as well as calling possible restarts of managed software themselves. For update_autohsts method, the installer may have to call prepare() to finalize the plugin initialization. Methods: enable_autohsts is called when the header is initially installed using a low max-age value. update_autohsts is called every time when Certbot is run using 'renew' verb. The max-age value should be increased over time using this method. deploy_autohsts is called for every lineage that has had its certificate renewed. A long HSTS max-age value should be set here, as we should be confident that the user is able to automatically renew their certificates. """ @abc.abstractmethod def update_autohsts(self, lineage: interfaces.RenewableCert, *args: Any, **kwargs: Any) -> None: """ Gets called for each lineage every time Certbot is run with 'renew' verb. Implementation of this method should increase the max-age value. :param lineage: Certificate lineage object :type lineage: certbot.interfaces.RenewableCert .. note:: prepare() method inherited from `interfaces.Plugin` might need to be called manually within implementation of this interface method to finalize the plugin initialization. """ @abc.abstractmethod def deploy_autohsts(self, lineage: interfaces.RenewableCert, *args: Any, **kwargs: Any) -> None: """ Gets called for a lineage when its certificate is successfully renewed. Long max-age value should be set in implementation of this method. :param lineage: Certificate lineage object :type lineage: certbot.interfaces.RenewableCert """ @abc.abstractmethod def enable_autohsts(self, lineage: Optional[interfaces.RenewableCert], domains: Iterable[str], *args: Any, **kwargs: Any) -> None: """ Enables the AutoHSTS enhancement, installing Strict-Transport-Security header with a low initial value to be increased over the subsequent runs of Certbot renew. :param lineage: Certificate lineage object :type lineage: certbot.interfaces.RenewableCert :param domains: List of domains in certificate to enhance :type domains: `list` of `str` """ # This is used to configure internal new style enhancements in Certbot. These # enhancement interfaces need to be defined in this file. Please do not modify # this list from plugin code. _INDEX: List[Dict[str, Any]] = [ { "name": "AutoHSTS", "cli_help": "Gradually increasing max-age value for HTTP Strict Transport "+ "Security security header", "cli_flag": "--auto-hsts", "cli_flag_default": constants.CLI_DEFAULTS["auto_hsts"], "cli_groups": ["security", "enhance"], "cli_dest": "auto_hsts", "cli_action": "store_true", "class": AutoHSTSEnhancement, "updater_function": "update_autohsts", "deployer_function": "deploy_autohsts", "enable_function": "enable_autohsts" } ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/plugins/storage.py0000664000175100017510000001030614561227515017331 0ustar00ericaerica"""Plugin storage class.""" import json import logging from typing import Any from typing import Dict from certbot import configuration from certbot import errors from certbot.compat import filesystem from certbot.compat import os logger = logging.getLogger(__name__) class PluginStorage: """Class implementing storage functionality for plugins""" def __init__(self, config: configuration.NamespaceConfig, classkey: str) -> None: """Initializes PluginStorage object storing required configuration options. :param .configuration.NamespaceConfig config: Configuration object :param str classkey: class name to use as root key in storage file """ self._config = config self._classkey = classkey self._initialized = False self._data: Dict self._storagepath: str def _initialize_storage(self) -> None: """Initializes PluginStorage data and reads current state from the disk if the storage json exists.""" self._storagepath = os.path.join(self._config.config_dir, ".pluginstorage.json") self._load() self._initialized = True def _load(self) -> None: """Reads PluginStorage content from the disk to a dict structure :raises .errors.PluginStorageError: when unable to open or read the file """ data: Dict[str, Any] = {} filedata = "" try: with open(self._storagepath, 'r') as fh: filedata = fh.read() except IOError as e: errmsg = "Could not read PluginStorage data file: {0} : {1}".format( self._storagepath, str(e)) if os.path.isfile(self._storagepath): # Only error out if file exists, but cannot be read logger.error(errmsg) raise errors.PluginStorageError(errmsg) try: data = json.loads(filedata) except ValueError: if not filedata: logger.debug("Plugin storage file %s was empty, no values loaded", self._storagepath) else: errmsg = "PluginStorage file {0} is corrupted.".format( self._storagepath) logger.error(errmsg) raise errors.PluginStorageError(errmsg) self._data = data def save(self) -> None: """Saves PluginStorage content to disk :raises .errors.PluginStorageError: when unable to serialize the data or write it to the filesystem """ if not self._initialized: errmsg = "Unable to save, no values have been added to PluginStorage." logger.error(errmsg) raise errors.PluginStorageError(errmsg) try: serialized = json.dumps(self._data) except TypeError as e: errmsg = "Could not serialize PluginStorage data: {0}".format( str(e)) logger.error(errmsg) raise errors.PluginStorageError(errmsg) try: with os.fdopen(filesystem.open( self._storagepath, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), 'w') as fh: fh.write(serialized) except IOError as e: errmsg = "Could not write PluginStorage data to file {0} : {1}".format( self._storagepath, str(e)) logger.error(errmsg) raise errors.PluginStorageError(errmsg) def put(self, key: str, value: Any) -> None: """Put configuration value to PluginStorage :param str key: Key to store the value to :param value: Data to store """ if not self._initialized: self._initialize_storage() if self._classkey not in self._data: self._data[self._classkey] = {} self._data[self._classkey][key] = value def fetch(self, key: str) -> Any: """Get configuration value from PluginStorage :param str key: Key to get value from the storage :raises KeyError: If the key doesn't exist in the storage """ if not self._initialized: self._initialize_storage() return self._data[self._classkey][key] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/plugins/util.py0000664000175100017510000000342314561227515016644 0ustar00ericaerica"""Plugin utilities.""" import logging from typing import List from certbot import util from certbot.compat import os from certbot.compat.misc import STANDARD_BINARY_DIRS logger = logging.getLogger(__name__) def get_prefixes(path: str) -> List[str]: """Retrieves all possible path prefixes of a path, in descending order of length. For instance: * (Linux) `/a/b/c` returns `['/a/b/c', '/a/b', '/a', '/']` * (Windows) `C:\\a\\b\\c` returns `['C:\\a\\b\\c', 'C:\\a\\b', 'C:\\a', 'C:']` :param str path: the path to break into prefixes :returns: all possible path prefixes of given path in descending order :rtype: `list` of `str` """ prefix = os.path.normpath(path) prefixes: List[str] = [] while prefix: prefixes.append(prefix) prefix, _ = os.path.split(prefix) # break once we hit the root path if prefix == prefixes[-1]: break return prefixes def path_surgery(cmd: str) -> bool: """Attempt to perform PATH surgery to find cmd Mitigates https://github.com/certbot/certbot/issues/1833 :param str cmd: the command that is being searched for in the PATH :returns: True if the operation succeeded, False otherwise """ path = os.environ["PATH"] added = [] for d in STANDARD_BINARY_DIRS: if d not in path: path += os.pathsep + d added.append(d) if any(added): logger.debug("Can't find %s, attempting PATH mitigation by adding %s", cmd, os.pathsep.join(added)) os.environ["PATH"] = path if util.exe_exists(cmd): return True expanded = " expanded" if any(added) else "" logger.debug("Failed to find executable %s in%s PATH: %s", cmd, expanded, path) return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/py.typed0000664000175100017510000000000014561227515015317 0ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/reverter.py0000664000175100017510000005306514561227515016053 0ustar00ericaerica"""Reverter class saves configuration checkpoints and allows for recovery.""" import csv import glob import logging import shutil import time import traceback from typing import Iterable from typing import List from typing import Set from typing import TextIO from typing import Tuple from certbot import configuration from certbot import errors from certbot import util from certbot._internal import constants from certbot.compat import filesystem from certbot.compat import os logger = logging.getLogger(__name__) class Reverter: """Reverter Class - save and revert configuration checkpoints. This class can be used by the plugins, especially Installers, to undo changes made to the user's system. Modifications to files and commands to do undo actions taken by the plugin should be registered with this class before the action is taken. Once a change has been registered with this class, there are three states the change can be in. First, the change can be a temporary change. This should be used for changes that will soon be reverted, such as config changes for the purpose of solving a challenge. Changes are added to this state through calls to :func:`~add_to_temp_checkpoint` and reverted when :func:`~revert_temporary_config` or :func:`~recovery_routine` is called. The second state a change can be in is in progress. These changes are not temporary, however, they also have not been finalized in a checkpoint. A change must become in progress before it can be finalized. Changes are added to this state through calls to :func:`~add_to_checkpoint` and reverted when :func:`~recovery_routine` is called. The last state a change can be in is finalized in a checkpoint. A change is put into this state by first becoming an in progress change and then calling :func:`~finalize_checkpoint`. Changes in this state can be reverted through calls to :func:`~rollback_checkpoints`. As a final note, creating new files and registering undo commands are handled specially and use the methods :func:`~register_file_creation` and :func:`~register_undo_command` respectively. Both of these methods can be used to create either temporary or in progress changes. .. note:: Consider moving everything over to CSV format. :param config: Configuration. :type config: :class:`certbot.configuration.NamespaceConfig` """ def __init__(self, config: configuration.NamespaceConfig) -> None: self.config = config util.make_or_verify_dir( config.backup_dir, constants.CONFIG_DIRS_MODE, self.config.strict_permissions) def revert_temporary_config(self) -> None: """Reload users original configuration files after a temporary save. This function should reinstall the users original configuration files for all saves with temporary=True :raises .ReverterError: when unable to revert config """ if os.path.isdir(self.config.temp_checkpoint_dir): try: self._recover_checkpoint(self.config.temp_checkpoint_dir) except errors.ReverterError: # We have a partial or incomplete recovery logger.critical( "Incomplete or failed recovery for %s", self.config.temp_checkpoint_dir, ) raise errors.ReverterError("Unable to revert temporary config") def rollback_checkpoints(self, rollback: int = 1) -> None: """Revert 'rollback' number of configuration checkpoints. :param int rollback: Number of checkpoints to reverse. A str num will be cast to an integer. So "2" is also acceptable. :raises .ReverterError: if there is a problem with the input or if the function is unable to correctly revert the configuration checkpoints """ try: rollback = int(rollback) except ValueError: logger.error("Rollback argument must be a positive integer") raise errors.ReverterError("Invalid Input") # Sanity check input if rollback < 0: logger.error("Rollback argument must be a positive integer") raise errors.ReverterError("Invalid Input") backups = os.listdir(self.config.backup_dir) backups.sort() if not backups: logger.warning( "Certbot hasn't modified your configuration, so rollback " "isn't available.") elif len(backups) < rollback: logger.warning("Unable to rollback %d checkpoints, only %d exist", rollback, len(backups)) while rollback > 0 and backups: cp_dir = os.path.join(self.config.backup_dir, backups.pop()) try: self._recover_checkpoint(cp_dir) except errors.ReverterError: logger.critical("Failed to load checkpoint during rollback") raise errors.ReverterError( "Unable to load checkpoint during rollback") rollback -= 1 def add_to_temp_checkpoint(self, save_files: Set[str], save_notes: str) -> None: """Add files to temporary checkpoint. :param set save_files: set of filepaths to save :param str save_notes: notes about changes during the save """ self._add_to_checkpoint_dir( self.config.temp_checkpoint_dir, save_files, save_notes) def add_to_checkpoint(self, save_files: Set[str], save_notes: str) -> None: """Add files to a permanent checkpoint. :param set save_files: set of filepaths to save :param str save_notes: notes about changes during the save """ # Check to make sure we are not overwriting a temp file self._check_tempfile_saves(save_files) self._add_to_checkpoint_dir( self.config.in_progress_dir, save_files, save_notes) def _add_to_checkpoint_dir(self, cp_dir: str, save_files: Set[str], save_notes: str) -> None: """Add save files to checkpoint directory. :param str cp_dir: Checkpoint directory filepath :param set save_files: set of files to save :param str save_notes: notes about changes made during the save :raises IOError: if unable to open cp_dir + FILEPATHS file :raises .ReverterError: if unable to add checkpoint """ util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, self.config.strict_permissions) op_fd, existing_filepaths = self._read_and_append( os.path.join(cp_dir, "FILEPATHS")) idx = len(existing_filepaths) for filename in save_files: # No need to copy/index already existing files # The oldest copy already exists in the directory... if filename not in existing_filepaths: # Tag files with index so multiple files can # have the same filename logger.debug("Creating backup of %s", filename) try: shutil.copy2(filename, os.path.join( cp_dir, os.path.basename(filename) + "_" + str(idx))) op_fd.write('{0}\n'.format(filename)) # https://stackoverflow.com/questions/4726260/effective-use-of-python-shutil-copy2 except IOError: op_fd.close() logger.error( "Unable to add file %s to checkpoint %s", filename, cp_dir) raise errors.ReverterError( "Unable to add file {0} to checkpoint " "{1}".format(filename, cp_dir)) idx += 1 op_fd.close() with open(os.path.join(cp_dir, "CHANGES_SINCE"), "a") as notes_fd: notes_fd.write(save_notes) def _read_and_append(self, filepath: str) -> Tuple[TextIO, List[str]]: """Reads the file lines and returns a file obj. Read the file returning the lines, and a pointer to the end of the file. """ # pylint: disable=consider-using-with # Open up filepath differently depending on if it already exists if os.path.isfile(filepath): op_fd = open(filepath, "r+") lines = op_fd.read().splitlines() else: lines = [] op_fd = open(filepath, "w") return op_fd, lines def _recover_checkpoint(self, cp_dir: str) -> None: """Recover a specific checkpoint. Recover a specific checkpoint provided by cp_dir Note: this function does not reload augeas. :param str cp_dir: checkpoint directory file path :raises errors.ReverterError: If unable to recover checkpoint """ # Undo all commands if os.path.isfile(os.path.join(cp_dir, "COMMANDS")): self._run_undo_commands(os.path.join(cp_dir, "COMMANDS")) # Revert all changed files if os.path.isfile(os.path.join(cp_dir, "FILEPATHS")): try: with open(os.path.join(cp_dir, "FILEPATHS")) as paths_fd: filepaths = paths_fd.read().splitlines() for idx, path in enumerate(filepaths): shutil.copy2(os.path.join( cp_dir, os.path.basename(path) + "_" + str(idx)), path) except (IOError, OSError): # This file is required in all checkpoints. logger.error("Unable to recover files from %s", cp_dir) raise errors.ReverterError(f"Unable to recover files from {cp_dir}") # Remove any newly added files if they exist self._remove_contained_files(os.path.join(cp_dir, "NEW_FILES")) try: shutil.rmtree(cp_dir) except OSError: logger.error("Unable to remove directory: %s", cp_dir) raise errors.ReverterError( "Unable to remove directory: %s" % cp_dir) def _run_undo_commands(self, filepath: str) -> None: """Run all commands in a file.""" # NOTE: csv module uses native strings. That is unicode on Python 3 # It is strongly advised to set newline = '' on Python 3 with CSV, # and it fixes problems on Windows. kwargs = {'newline': ''} with open(filepath, 'r', **kwargs) as csvfile: # type: ignore csvreader = csv.reader(csvfile) for command in reversed(list(csvreader)): try: util.run_script(command) except errors.SubprocessError: logger.error( "Unable to run undo command: %s", " ".join(command)) def _check_tempfile_saves(self, save_files: Set[str]) -> None: """Verify save isn't overwriting any temporary files. :param set save_files: Set of files about to be saved. :raises certbot.errors.ReverterError: when save is attempting to overwrite a temporary file. """ protected_files = [] # Get temp modified files temp_path = os.path.join(self.config.temp_checkpoint_dir, "FILEPATHS") if os.path.isfile(temp_path): with open(temp_path, "r") as protected_fd: protected_files.extend(protected_fd.read().splitlines()) # Get temp new files new_path = os.path.join(self.config.temp_checkpoint_dir, "NEW_FILES") if os.path.isfile(new_path): with open(new_path, "r") as protected_fd: protected_files.extend(protected_fd.read().splitlines()) # Verify no save_file is in protected_files for filename in protected_files: if filename in save_files: raise errors.ReverterError(f"Attempting to overwrite challenge file - {filename}") def register_file_creation(self, temporary: bool, *files: str) -> None: r"""Register the creation of all files during certbot execution. Call this method before writing to the file to make sure that the file will be cleaned up if the program exits unexpectedly. (Before a save occurs) :param bool temporary: If the file creation registry is for a temp or permanent save. :param \*files: file paths (str) to be registered :raises certbot.errors.ReverterError: If call does not contain necessary parameters or if the file creation is unable to be registered. """ # Make sure some files are provided... as this is an error # Made this mistake in my initial implementation of apache.dvsni.py if not files: raise errors.ReverterError("Forgot to provide files to registration call") cp_dir = self._get_cp_dir(temporary) # Append all new files (that aren't already registered) new_fd = None try: new_fd, ex_files = self._read_and_append(os.path.join(cp_dir, "NEW_FILES")) for path in files: if path not in ex_files: new_fd.write("{0}\n".format(path)) except (IOError, OSError): logger.error("Unable to register file creation(s) - %s", files) raise errors.ReverterError( "Unable to register file creation(s) - {0}".format(files)) finally: if new_fd is not None: new_fd.close() def register_undo_command(self, temporary: bool, command: Iterable[str]) -> None: """Register a command to be run to undo actions taken. .. warning:: This function does not enforce order of operations in terms of file modification vs. command registration. All undo commands are run first before all normal files are reverted to their previous state. If you need to maintain strict order, you may create checkpoints before and after the the command registration. This function may be improved in the future based on demand. :param bool temporary: Whether the command should be saved in the IN_PROGRESS or TEMPORARY checkpoints. :param command: Command to be run. :type command: list of str """ commands_fp = os.path.join(self._get_cp_dir(temporary), "COMMANDS") # It is strongly advised to set newline = '' on Python 3 with CSV, # and it fixes problems on Windows. kwargs = {'newline': ''} try: mode = "a" if os.path.isfile(commands_fp) else "w" with open(commands_fp, mode, **kwargs) as f: # type: ignore csvwriter = csv.writer(f) csvwriter.writerow(command) except (IOError, OSError): logger.error("Unable to register undo command") raise errors.ReverterError( "Unable to register undo command.") def _get_cp_dir(self, temporary: bool) -> str: """Return the proper reverter directory.""" if temporary: cp_dir = self.config.temp_checkpoint_dir else: cp_dir = self.config.in_progress_dir util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, self.config.strict_permissions) return cp_dir def recovery_routine(self) -> None: """Revert configuration to most recent finalized checkpoint. Remove all changes (temporary and permanent) that have not been finalized. This is useful to protect against crashes and other execution interruptions. :raises .errors.ReverterError: If unable to recover the configuration """ # First, any changes found in NamespaceConfig.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_temporary_config() if os.path.isdir(self.config.in_progress_dir): try: self._recover_checkpoint(self.config.in_progress_dir) except errors.ReverterError: # We have a partial or incomplete recovery logger.critical("Incomplete or failed recovery for IN_PROGRESS " "checkpoint - %s", self.config.in_progress_dir) raise errors.ReverterError( "Incomplete or failed recovery for IN_PROGRESS checkpoint " "- %s" % self.config.in_progress_dir) def _remove_contained_files(self, file_list: str) -> bool: """Erase all files contained within file_list. :param str file_list: file containing list of file paths to be deleted :returns: Success :rtype: bool :raises certbot.errors.ReverterError: If all files within file_list cannot be removed """ # 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 list_fd: filepaths = list_fd.read().splitlines() for path in filepaths: # Files are registered before they are added... so # check to see if file exists first if os.path.lexists(path): os.remove(path) else: logger.warning( "File: %s - Could not be found to be deleted\n" " - Certbot probably shut down unexpectedly", path) except (IOError, OSError): logger.critical( "Unable to remove filepaths contained within %s", file_list) raise errors.ReverterError( "Unable to remove filepaths contained within " "{0}".format(file_list)) return True def finalize_checkpoint(self, title: str) -> None: """Finalize the checkpoint. Timestamps and permanently saves all changes made through the use of :func:`~add_to_checkpoint` and :func:`~register_file_creation` :param str title: Title describing checkpoint :raises certbot.errors.ReverterError: when the checkpoint is not able to be finalized. """ # Check to make sure an "in progress" directory exists if not os.path.isdir(self.config.in_progress_dir): return changes_since_path = os.path.join(self.config.in_progress_dir, "CHANGES_SINCE") changes_since_tmp_path = os.path.join(self.config.in_progress_dir, "CHANGES_SINCE.tmp") if not os.path.exists(changes_since_path): logger.info("Rollback checkpoint is empty (no changes made?)") with open(changes_since_path, 'w') as f: f.write("No changes\n") # Add title to self.config.in_progress_dir CHANGES_SINCE try: with open(changes_since_tmp_path, "w") as changes_tmp: changes_tmp.write("-- %s --\n" % title) with open(changes_since_path, "r") as changes_orig: changes_tmp.write(changes_orig.read()) # Move self.config.in_progress_dir to Backups directory shutil.move(changes_since_tmp_path, changes_since_path) except (IOError, OSError): logger.error("Unable to finalize checkpoint - adding title") logger.debug("Exception was:\n%s", traceback.format_exc()) raise errors.ReverterError("Unable to add title") # rename the directory as a timestamp self._timestamp_progress_dir() def _checkpoint_timestamp(self) -> str: "Determine the timestamp of the checkpoint, enforcing monotonicity." timestamp = str(time.time()) others = glob.glob(os.path.join(self.config.backup_dir, "[0-9]*")) others = [os.path.basename(d) for d in others] others.append(timestamp) others.sort() if others[-1] != timestamp: timetravel = str(float(others[-1]) + 1) logger.warning("Current timestamp %s does not correspond to newest reverter " "checkpoint; your clock probably jumped. Time travelling to %s", timestamp, timetravel) timestamp = timetravel elif len(others) > 1 and others[-2] == timestamp: # It is possible if the checkpoints are made extremely quickly # that will result in a name collision. logger.debug("Race condition with timestamp %s, incrementing by 0.01", timestamp) timetravel = str(float(others[-1]) + 0.01) timestamp = timetravel return timestamp def _timestamp_progress_dir(self) -> None: """Timestamp the checkpoint.""" # It is possible save checkpoints faster than 1 per second resulting in # collisions in the naming convention. for _ in range(2): timestamp = self._checkpoint_timestamp() final_dir = os.path.join(self.config.backup_dir, timestamp) try: filesystem.replace(self.config.in_progress_dir, final_dir) return except OSError: logger.warning("Unexpected race condition, retrying (%s)", timestamp) # After 10 attempts... something is probably wrong here... logger.error( "Unable to finalize checkpoint, %s -> %s", self.config.in_progress_dir, final_dir) raise errors.ReverterError( "Unable to finalize checkpoint renaming") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/ssl-dhparams.pem0000664000175100017510000000065014561227515016734 0ustar00ericaerica-----BEGIN DH PARAMETERS----- MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz +8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a 87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi 7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== -----END DH PARAMETERS----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3350835 certbot-2.9.0/certbot/tests/0000775000175100017510000000000014561227516014775 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/__init__.py0000664000175100017510000000005214561227515017102 0ustar00ericaerica"""Utilities for running Certbot tests""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/acme_util.py0000664000175100017510000000531314561227515017312 0ustar00ericaerica"""ACME utilities for testing.""" import datetime from typing import Any from typing import Dict from typing import Iterable import josepy as jose from acme import challenges from acme import messages from certbot._internal import auth_handler from certbot.tests import util JWK = jose.JWK.load(util.load_vector('rsa512_key.pem')) KEY = util.load_rsa_private_key('rsa512_key.pem') # Challenges HTTP01 = challenges.HTTP01( token=b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") DNS01 = challenges.DNS01(token=b"17817c66b60ce2e4012dfad92657527a") DNS01_2 = challenges.DNS01(token=b"cafecafecafecafecafecafe0feedbac") CHALLENGES = [HTTP01, DNS01] def chall_to_challb(chall: challenges.Challenge, status: messages.Status) -> messages.ChallengeBody: """Return ChallengeBody from Challenge.""" kwargs = { "chall": chall, "uri": chall.typ + "_uri", "status": status, } if status == messages.STATUS_VALID: kwargs.update({"validated": datetime.datetime.now()}) return messages.ChallengeBody(**kwargs) # Pending ChallengeBody objects HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING) DNS01_P_2 = chall_to_challb(DNS01_2, messages.STATUS_PENDING) CHALLENGES_P = [HTTP01_P, DNS01_P] # AnnotatedChallenge objects HTTP01_A = auth_handler.challb_to_achall(HTTP01_P, JWK, "example.com") DNS01_A = auth_handler.challb_to_achall(DNS01_P, JWK, "example.org") DNS01_A_2 = auth_handler.challb_to_achall(DNS01_P_2, JWK, "esimerkki.example.org") ACHALLENGES = [HTTP01_A, DNS01_A] def gen_authzr(authz_status: messages.Status, domain: str, challs: Iterable[challenges.Challenge], statuses: Iterable[messages.Status]) -> messages.AuthorizationResource: """Generate an authorization resource. :param authz_status: Status object :type authz_status: :class:`acme.messages.Status` :param list challs: Challenge objects :param list statuses: status of each challenge object """ challbs = tuple( chall_to_challb(chall, status) for chall, status in zip(challs, statuses) ) authz_kwargs: Dict[str, Any] = { "identifier": messages.Identifier( typ=messages.IDENTIFIER_FQDN, value=domain), "challenges": challbs, } if authz_status == messages.STATUS_VALID: authz_kwargs.update({ "status": authz_status, "expires": datetime.datetime.now() + datetime.timedelta(days=31), }) else: authz_kwargs.update({ "status": authz_status, }) return messages.AuthorizationResource( uri="https://trusted.ca/new-authz-resource", body=messages.Authorization(**authz_kwargs) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3390837 certbot-2.9.0/certbot/tests/testdata/0000775000175100017510000000000014561227516016606 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/README0000664000175100017510000000120214561227515017460 0ustar00ericaericaThe following command has been used to generate test keys: for x in 256 512 2048; do openssl genrsa -out rsa${k}_key.pem $k; done For the elliptic curve private keys, this command was used: for k in "prime256v1" "secp384r1" "secp521r1" do openssl genpkey -algorithm ${k} -out ec_${k}_key.pem done and for the CSR PEM (Certificate Signing Request): openssl req -new -out csr-Xsans_X.pem -key rsa512_key.pem [-config csr-Xsans_X.conf | -subj '/CN=example.com'] [-outform DER > csr_X.der] and for the certificate: openssl req -new -out cert_X.pem -key rsaX_key.pem -subj '/CN=example.com' -x509 [-outform DER > cert_X.der] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert-5sans_512.pem0000664000175100017510000000167014561227515021667 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIICkTCCAjugAwIBAgIJAJNbfABWQ8bbMA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNV BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp c2NvMScwJQYDVQQKDB5FbGVjdHJvbmljIEZyb250aWVyIEZvdW5kYXRpb24xFDAS BgNVBAMMC2V4YW1wbGUuY29tMB4XDTE2MDYwOTIzMDEzNloXDTE2MDcwOTIzMDEz NloweTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM DVNhbiBGcmFuY2lzY28xJzAlBgNVBAoMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91 bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANL ADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE 30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4GlMIGiMB0GA1UdDgQWBBQmz8jt S9eUsuQlA1gkjwTAdNWXijAfBgNVHSMEGDAWgBQmz8jtS9eUsuQlA1gkjwTAdNWX ijAMBgNVHRMEBTADAQH/MFIGA1UdEQRLMEmCDWEuZXhhbXBsZS5jb22CDWIuZXhh bXBsZS5jb22CDWMuZXhhbXBsZS5jb22CDWQuZXhhbXBsZS5jb22CC2V4YW1wbGUu Y29tMA0GCSqGSIb3DQEBCwUAA0EAVXmZxB+IJdgFvY2InOYeytTD1QmouDZRtj/T H/HIpSdsfO7qr4d/ZprI2IhLRxp2S4BiU5Qc5HUkeADcpNd06A== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert-nosans_nistp256.pem0000664000175100017510000000115714561227515023222 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIBoDCCAUYCCQDCnzfUZ7TQdDAKBggqhkjOPQQDAjBYMQswCQYDVQQGEwJVUzER MA8GA1UECAwITWljaGlnYW4xEjAQBgNVBAcMCUFubiBBcmJvcjEMMAoGA1UECgwD RUZGMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xODA1MTUxNzIyMzlaFw0xODA2 MTQxNzIyMzlaMFgxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjESMBAG A1UEBwwJQW5uIEFyYm9yMQwwCgYDVQQKDANFRkYxFDASBgNVBAMMC2V4YW1wbGUu Y29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPPl0JauSZukvAUWv4l5VNLAY QXhuPXYQBf4dVET3s0E5q9ZCbSe+pNUbko9F+TFkuc7XVjQPsfkDbh0I9nD0tzAK BggqhkjOPQQDAgNIADBFAiEAv8S2GXmWJqZ+j3DBfm72E1YK+HkOf+TOUHsbVR+O Z1oCIFWNt1SPdIgRp4QAyzVk2pcTF8jDNajEMLWETDtxgRvM -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert-san_512.pem0000664000175100017510000000142214561227515021412 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7 RDjyGMKy5ZgM2w== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert_2048.pem0000664000175100017510000000226014561227515020722 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIDSjCCAjKgAwIBAgIJAIYLtIQHBBG0MA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV BAYTAkNBMQswCQYDVQQIDAJPTjEQMA4GA1UEBwwHVG9yb250bzEMMAoGA1UECgwD RUZGMB4XDTE3MDUyOTA3NDIwMVoXDTQ4MDMzMDA3NDIwMVowOjELMAkGA1UEBhMC Q0ExCzAJBgNVBAgMAk9OMRAwDgYDVQQHDAdUb3JvbnRvMQwwCgYDVQQKDANFRkYw ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm1WIecnHjL4FsJvxDP27G yeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop+D7s+oh0 apV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3DaNokGn7 r3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf5QU5pFx6 0a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDVMJ3mIB8F OW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf3nCJFk1f AgMBAAGjUzBRMB0GA1UdDgQWBBSdJ++M23AW3LkFD7LKhsH7gL6/2jAfBgNVHSME GDAWgBSdJ++M23AW3LkFD7LKhsH7gL6/2jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG SIb3DQEBCwUAA4IBAQCV5kSt1HTFzUPdBvxT455YrLd3jIsRt1pRNuGjVaUYIRxh vds8NN1Z8h/8Cdzz8NVkIdCuYb2lFaDjs3zNVUQxCyVcH7xVyPwFI85NR27+HPRv xzz2rwzST+NKYst6ZBg086BKjqFtxs16lpU/TD6tOJqg86TBbfP6gib/ocGeER2D HEEik69FjmUCziT6uXyYW5y1PxD15UWO3RWoTpao0vGtTPceTeeuO05PVeCUlx8X YXg9zoVWBba0GF+qQJ67zT5nvfc2KJcgnWRIRr/90YXzBf+FdFVuC4xFHINBI1OJ 5XBLJOv61Zu+Du/nmlBVcb8KL/Vd2oZyfoH+0oCN -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert_512.pem0000664000175100017510000000130514561227515020633 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn B/o= -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert_512_bad.pem0000664000175100017510000000156714561227515021453 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIICYzCCAg2gAwIBAgIJAPvqv4TcAtuFMA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD VQQGEwJDQTEQMA4GA1UECAwHT250YXJpbzEQMA4GA1UEBwwHVG9yb250bzEMMAoG A1UECgwDRUZGMRYwFAYDVQQLDA1UZWNoIFByb2plY3RzMQ4wDAYDVQQDDAVZb21u YTEjMCEGCSqGSIb3DQEJARYUeW9tbmEubmFzc2VyQGVmZi5vcmcwHhcNMTcwMzI0 MjIzMjUxWhcNNDgwMTI0MjIzMjUxWjCBjDELMAkGA1UEBhMCQ0ExEDAOBgNVBAgM B09udGFyaW8xEDAOBgNVBAcMB1Rvcm9udG8xDDAKBgNVBAoMA0VGRjEWMBQGA1UE CwwNVGVjaCBQcm9qZWN0czEOMAwGA1UEAwwFWW9tbmExIzAhBgkqhkiG9w0BCQEW FHlvbW5hLm5hc3NlckBlZmYub3JnMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1 c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvO jm0c+pVE6K+EdE/twuUCAwEAAaNQME4wHQYDVR0OBBYEFCbPyO1L15Sy5CUDWCSP BMB01ZeKMB8GA1UdIwQYMBaAFCbPyO1L15Sy5CUDWCSPBMB01ZeKMAwGA1UdEwQF MAMBAf8wDQYJKoZIhvcNAQELBQADQQAeWDdcrJOolFHr3m8TrlDJ/Ca4SfJya2jb K1wahbX83sC42834HbDOQASGBhoLYDhC1cMPbKDDjMbR9rjYuf7T -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert_fullchain_2048.pem0000664000175100017510000000454014561227515022752 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIDSjCCAjKgAwIBAgIJAIYLtIQHBBG0MA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV BAYTAkNBMQswCQYDVQQIDAJPTjEQMA4GA1UEBwwHVG9yb250bzEMMAoGA1UECgwD RUZGMB4XDTE3MDUyOTA3NDIwMVoXDTQ4MDMzMDA3NDIwMVowOjELMAkGA1UEBhMC Q0ExCzAJBgNVBAgMAk9OMRAwDgYDVQQHDAdUb3JvbnRvMQwwCgYDVQQKDANFRkYw ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm1WIecnHjL4FsJvxDP27G yeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop+D7s+oh0 apV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3DaNokGn7 r3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf5QU5pFx6 0a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDVMJ3mIB8F OW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf3nCJFk1f AgMBAAGjUzBRMB0GA1UdDgQWBBSdJ++M23AW3LkFD7LKhsH7gL6/2jAfBgNVHSME GDAWgBSdJ++M23AW3LkFD7LKhsH7gL6/2jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG SIb3DQEBCwUAA4IBAQCV5kSt1HTFzUPdBvxT455YrLd3jIsRt1pRNuGjVaUYIRxh vds8NN1Z8h/8Cdzz8NVkIdCuYb2lFaDjs3zNVUQxCyVcH7xVyPwFI85NR27+HPRv xzz2rwzST+NKYst6ZBg086BKjqFtxs16lpU/TD6tOJqg86TBbfP6gib/ocGeER2D HEEik69FjmUCziT6uXyYW5y1PxD15UWO3RWoTpao0vGtTPceTeeuO05PVeCUlx8X YXg9zoVWBba0GF+qQJ67zT5nvfc2KJcgnWRIRr/90YXzBf+FdFVuC4xFHINBI1OJ 5XBLJOv61Zu+Du/nmlBVcb8KL/Vd2oZyfoH+0oCN -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDSjCCAjKgAwIBAgIJAIYLtIQHBBG0MA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV BAYTAkNBMQswCQYDVQQIDAJPTjEQMA4GA1UEBwwHVG9yb250bzEMMAoGA1UECgwD RUZGMB4XDTE3MDUyOTA3NDIwMVoXDTQ4MDMzMDA3NDIwMVowOjELMAkGA1UEBhMC Q0ExCzAJBgNVBAgMAk9OMRAwDgYDVQQHDAdUb3JvbnRvMQwwCgYDVQQKDANFRkYw ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm1WIecnHjL4FsJvxDP27G yeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop+D7s+oh0 apV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3DaNokGn7 r3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf5QU5pFx6 0a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDVMJ3mIB8F OW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf3nCJFk1f AgMBAAGjUzBRMB0GA1UdDgQWBBSdJ++M23AW3LkFD7LKhsH7gL6/2jAfBgNVHSME GDAWgBSdJ++M23AW3LkFD7LKhsH7gL6/2jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG SIb3DQEBCwUAA4IBAQCV5kSt1HTFzUPdBvxT455YrLd3jIsRt1pRNuGjVaUYIRxh vds8NN1Z8h/8Cdzz8NVkIdCuYb2lFaDjs3zNVUQxCyVcH7xVyPwFI85NR27+HPRv xzz2rwzST+NKYst6ZBg086BKjqFtxs16lpU/TD6tOJqg86TBbfP6gib/ocGeER2D HEEik69FjmUCziT6uXyYW5y1PxD15UWO3RWoTpao0vGtTPceTeeuO05PVeCUlx8X YXg9zoVWBba0GF+qQJ67zT5nvfc2KJcgnWRIRr/90YXzBf+FdFVuC4xFHINBI1OJ 5XBLJOv61Zu+Du/nmlBVcb8KL/Vd2oZyfoH+0oCN -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert_intermediate_1.pem0000664000175100017510000000227014561227515023220 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIDUDCCAjigAwIBAgIIYbnTKswm95swDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVUGViYmxlIFJvb3QgQ0EgNzcwNjgzMCAXDTIwMDYxOTIxMDY1NVoYDzIwNTAw NjE5MjAwNjU1WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDI1 OWJjMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALFeijP7HyxcUMrk CuKgEqyaRmgjGj3JvPnm7RPJl0j6rgWe767eweP2dMJ8LnYHWZVDwmkysq1rb/kS HuWoqUW1gpqwo4+ARMAb0fhx79Ze7eNp5M44PRN9SenK3TZQYrNFzC5t0NzNQaq8 ksv0R4kxi2VqfPuV6GCEYHI2t5z7249U+4PHEPSLEjq0TvFYp8aLVJ2cRLsspdXd ANQwbKCWz00j03CcK79LXR6ncNw1WqEoYxjzmagiAKBf9kJVawxeJF91A+kPH052 hzGxhmMOU/VeWcl+zGeRR2cybyMF1qZ/tDX0n+J4q7FrcOjzJeZH8uxwOZ4N7Xn2 lqavdTkCAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMCE8MSR KqrGnv0Vd4iv8FCVtVmhMB8GA1UdIwQYMBaAFBg/Nkzipd7/vMs/f9VV11uPo2bS MA0GCSqGSIb3DQEBCwUAA4IBAQBISrMR9Fchj2u1FhxAr7eQsdM1Lus3B/eEJmcR KDO/tNzns5zsrcJL42wzDVQA61X+aVzZBSfb1oMbwHCpWBvj88avL/mJ1OgrC+VZ v7IDdKOwvXmFf0VVE5LLCPY+gfR65LVRkb0W9ZrZ9Oj0ke9A9ryPSNTYBvxE+Qar V5Jqe0YdG7IQBd4xFFnpWZyEPnQBW2Sl5oudt6LgJaQosMJdRLp4lL805G/lKPEt CKjWoaP6J4V1Ty+zwuWVdIpSUuIoWkZ7BeIIqVkTg7ieNj4eN2zddDxbTIquFD2K 7rZ1691krTWTe8bkLx6wGaAfvLOk97Ywmdsot3nTVRbaVTXq -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert_intermediate_2.pem0000664000175100017510000000227014561227515023221 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIDUDCCAjigAwIBAgIILrJTDiWZDFkwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVUGViYmxlIFJvb3QgQ0EgMGNjNmYwMCAXDTIwMDYxOTIxMDY1NFoYDzIwNTAw NjE5MjAwNjU0WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDI1 OWJjMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALFeijP7HyxcUMrk CuKgEqyaRmgjGj3JvPnm7RPJl0j6rgWe767eweP2dMJ8LnYHWZVDwmkysq1rb/kS HuWoqUW1gpqwo4+ARMAb0fhx79Ze7eNp5M44PRN9SenK3TZQYrNFzC5t0NzNQaq8 ksv0R4kxi2VqfPuV6GCEYHI2t5z7249U+4PHEPSLEjq0TvFYp8aLVJ2cRLsspdXd ANQwbKCWz00j03CcK79LXR6ncNw1WqEoYxjzmagiAKBf9kJVawxeJF91A+kPH052 hzGxhmMOU/VeWcl+zGeRR2cybyMF1qZ/tDX0n+J4q7FrcOjzJeZH8uxwOZ4N7Xn2 lqavdTkCAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMCE8MSR KqrGnv0Vd4iv8FCVtVmhMB8GA1UdIwQYMBaAFE4E9PDqHbaTzFi8XEeTc/s/xEal MA0GCSqGSIb3DQEBCwUAA4IBAQCIEnLk9ZgIzwjew3ktKmZ/lDKl00EcIJ/or/GE IM3exvEzUYJotoCKdw/6d1mjfsJ1AkJUrY5WASEhXgQ9wP5/z08xwmCSVowSnHhg iKeQf6iNIqpxg7LGqOeSc32QXzeBMTsvQcqupZ7J6ptomiPopeAfYIMQWni9Ym/I P1ZjydR2petIFioPuFtByu1UhvIcF4LCJ7UWwhVNcrivf5VGaRUVac4IxGZ4Urw+ W1xgT8pt5Rk1yCoqXsenllXRxdakbDDG9F0ZBWQNmvArYZJt1mdq1dBHT/JO/wKI aGF5PXa6k3YwhyvK4zTDcwPKmOBikL2FgzlohLYfETpTznW0 -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cert_leaf.pem0000664000175100017510000000230414561227515021233 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIDWTCCAkGgAwIBAgIIYWk/LgSO+zowDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAyNTliYzEwHhcNMjAwNjIxMDEyMTM3 WhcNMjUwNjIxMDEyMTM3WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBAOABmyVITyczj6/R/1EWH6FGEfzJgOFi zUC1HweYouEnC3Pu4SV/qFyKC8FsVQAY5FYWV4rrexnMgAeCL7XBTFyvIYQ7FoCH 5SeyUr3T1j/BAaFZtdb419d+BXLBOYigIu36pyRrH/3z0oxvv0brG7zXty/AOHTK /k5aE7e3iuUSPJqNoGIQi4NwtNw5Fwcpe4lOHRxNNp9kfjxKYzLgj9xaT6cw6r2T 9QJ7wnU9C0/cs9h98xz+IhApyoVWlF4SE87dpG99NQ8M92od5xv1jSIF34AMQzww fAW3FIKr4onaMP6XHtiXnJFn4pSw+BryxPqsKz864Ritvokz/nJoylUCAwEAAaOB mDCBlTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKG2yAaJtpPVStXakLLD5Jc7fcaN MB8GA1UdIwQYMBaAFMCE8MSRKqrGnv0Vd4iv8FCVtVmhMBYGA1UdEQQPMA2CC2V4 YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBde/j8lgRqzksphFVwfx310TaD 4foS+15cq0OnTM/iVt1JtjyyGaRamgCcFXkm31IP8c9vnaReYGY/QawshK1NSLii W6mN/RqguQNoovp0xw3SkDb+lyDo0VuIyMrbwlbhP4P5uue+Shb1ZTy7QmfUeYuK OTjFI3j0oBiSKins3ryEFHIMvuXcaQ4F2uw6UOHBgwv5XbVV5B9l62jGbZnxrpc7 SB9mzU8I5CJoK1uetwuADwlhDX8ITQ778unR7yIZOojyIMKWd9z9KtKDVSPK9I+d u0oYF4xG97nTks6Cqt2OUJizSY3ojgwK4U8w1795FCVUlq5Z58gpWf1BhA8a -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/cli.ini0000664000175100017510000000003114561227515020047 0ustar00ericaericaagree-dev-preview = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/csr-6sans_512.conf0000664000175100017510000000076614561227515021673 0ustar00ericaerica[req] distinguished_name = req_distinguished_name req_extensions = v3_req [req_distinguished_name] C=US C_default = US ST=Michigan ST_default=Michigan L=Ann Arbor L_default=Ann Arbor O=EFF O_default=EFF OU=University of Michigan OU_default=University of Michigan CN=example.com CN_default=example.com [ v3_req ] subjectAltName = @alt_names [alt_names] DNS.1 = example.com DNS.2 = example.org DNS.3 = example.net DNS.4 = example.info DNS.5 = subdomain.example.com DNS.6 = other.subdomain.example.com././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/csr-6sans_512.pem0000664000175100017510000000124414561227515021517 0ustar00ericaerica-----BEGIN CERTIFICATE REQUEST----- MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoIGGMIGDBgkqhkiG 9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQA+ sU6T30n3SsdnHlj0Va8eECOWK7Lf8nUfxxgjPMQ7BoU8gbAnGfDmOlwDronTRqf1 Me+nlYJU4TX1OiX10DYu -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/csr-nonames_512.pem0000664000175100017510000000064414561227515022130 0ustar00ericaerica-----BEGIN CERTIFICATE REQUEST----- MIH/MIGqAgEAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEF AANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+ 6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoAAwDQYJKoZIhvcNAQELBQAD QQBt9XLSZ9DGfWcGGaBUTCiSY7lWBegpNlCeo8pK3ydWmKpjcza+j7lF5paph2LH lKWVQ8+xwYMscGWK0NApHGco -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/csr-nosans_512.conf0000664000175100017510000000042514561227515022132 0ustar00ericaerica[req] distinguished_name = req_distinguished_name [req_distinguished_name] C=US C_default = US ST=Michigan ST_default=Michigan L=Ann Arbor L_default=Ann Arbor O=EFF O_default=EFF OU=University of Michigan OU_default=University of Michigan CN=example.com CN_default=example.com././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/csr-nosans_512.pem0000664000175100017510000000075514561227515021774 0ustar00ericaerica-----BEGIN CERTIFICATE REQUEST----- MIIBMzCB3gIBADB5MQswCQYDVQQGEwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQ BgNVBAcMCUFubiBBcmJvcjEMMAoGA1UECgwDRUZGMR8wHQYDVQQLDBZVbml2ZXJz aXR5IHBmIE1pY2hpZ2FuMRQwEgYDVQQDDAtleGFtcGxlLmNvbTBcMA0GCSqGSIb3 DQEBAQUAA0sAMEgCQQCsdXO0Ue0f3a5wUkP838db0Cx1GxS4dQEEEOUfA2VF3d+n nzSu/b7pBYTfRxaB2YlLzo5tHPqVROivhHRP7cLlAgMBAAGgADANBgkqhkiG9w0B AQsFAANBAG06jIPvSC6wiGLy7sUTaEX4UCE6Cztp3vh/uXN7Q++CGn6KiXNs/BRW eFlcFPbvxbVG/ZZFR5aPs+Oy6RhqOjg= -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/csr-nosans_nistp256.pem0000664000175100017510000000070414561227515023051 0ustar00ericaerica-----BEGIN CERTIFICATE REQUEST----- MIIBFDCBugIBADBYMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQ BgNVBAcMCUFubiBBcmJvcjEMMAoGA1UECgwDRUZGMRQwEgYDVQQDDAtleGFtcGxl LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDz5dCWrkmbpLwFFr+JeVTSw GEF4bj12EAX+HVRE97NBOavWQm0nvqTVG5KPRfkxZLnO11Y0D7H5A24dCPZw9Leg ADAKBggqhkjOPQQDAgNJADBGAiEAuoZHrYA5sy2DRTdLAxJTBNHKFFKbtaGt+QaJ A62qa8sCIQCUkSgSAiNaEnJ7r5fKphdjeORHqhpl6flYkLE3lGmGdg== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/csr-san_512.pem0000664000175100017510000000107614561227515021251 0ustar00ericaerica-----BEGIN CERTIFICATE REQUEST----- MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/csr_512.der0000664000175100017510000000043114561227515020455 0ustar00ericaerica0‚0À0[1 0 UAU10U Some-State1!0U Internet Widgits Pty Ltd10U Example.com0\0  *†H†÷ K0HA¬us´QíÝ®pRCüßÇ[Ð,u¸uåeEÝß§Ÿ4®ý¾é„ßGÙ‰KÎŽmú•D评tOíÂå 0  *†H†÷  ARZ¸-¯*j"´ àÏÜÍ}…s2lö¡é†Ùó¯>, trÔ–C¦Ì¥Có”¡pX·Ô.o–{O;^†ºbá././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/csr_512.pem0000664000175100017510000000070414561227515020467 0ustar00ericaerica-----BEGIN CERTIFICATE REQUEST----- MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtFeGFt cGxlLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCsdXO0Ue0f3a5wUkP838db 0Cx1GxS4dQEEEOUfA2VF3d+nnzSu/b7pBYTfRxaB2YlLzo5tHPqVROivhHRP7cLl AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAAceUlq4La8qaiK0DeDP3M19BIVzMmz2 oemG2fOvPiwNCB90ctSWQ6bMpUMV85ShcFi31C5vlntPfztehhq6YuE= -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/ec_prime256v1_key.pem0000664000175100017510000000045614561227515022454 0ustar00ericaerica-----BEGIN EC PARAMETERS----- BggqhkjOPQMBBw== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MHcCAQEEIDqQPQl69kuh+DrecC8SFPt21f0F/HHDP3T4/Lf0zIVFoAoGCCqGSM49 AwEHoUQDQgAEHou50Ee9u+8Vial6VbUHExlzsiCHtORlW0X0pKo5RspIKB0QyKwo dUXvBbv95I9yCO5+MlGkKjwLHtIEze0Hww== -----END EC PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/ec_secp384r1_key.pem0000664000175100017510000000054714561227515022271 0ustar00ericaerica-----BEGIN EC PARAMETERS----- BgUrgQQAIg== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MIGkAgEBBDAgvZGw5C7Mp26N0cXA+vIg5K/J5MJw+MVGYfGF4ZutuCLeYMrWT68R A0h6hJvDtMSgBwYFK4EEACKhZANiAAR1uQYZeU5Kml5o53Q8/PCdwUbqdgCSkV0C J5a6bhDRMp20fdp2T/mbkdxuVEl81lqfKPZhsd4CZsLaVIU3RUoGgIT1R3QKawpJ SuXq37yWFX2hqlgt+lsBufZ8RD5QnZc= -----END EC PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/ec_secp521r1_key.pem0000664000175100017510000000066414561227515022262 0ustar00ericaerica-----BEGIN EC PARAMETERS----- BgUrgQQAIw== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MIHcAgEBBEIACWWVKm1qAIejZ6qmqk9D69wQW5FAe3Er0IxWAMkonTEhu8EH5Q2i 2vT2bESm730zhGTe2Pn11b85H6UI9hxhCHygBwYFK4EEACOhgYkDgYYABAEQi1WF m3suHjPyWACyOJYGUn1Kx6rfBo0PjC7X2TU9jr8umLkIpaaF5UsBuMBmdz1IHL0U k0gQtoOQ0Qu8N74GuAGzGR0S3RYIv6gfYVz3dS1K4n4b307Lx62bnvtlNxcIvt3w hmS5OdvQ1Kdxh6oqbSVhhbQmJcgab78Txx3R2QeCxw== -----END EC PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/nistp256_key.pem0000664000175100017510000000034314561227515021552 0ustar00ericaerica-----BEGIN EC PRIVATE KEY----- MHcCAQEEIOvXH384CyNNv2lfxvjc7hg2f7ScYoLvlk/VpINLJlGBoAoGCCqGSM49 AwEHoUQDQgAEPPl0JauSZukvAUWv4l5VNLAYQXhuPXYQBf4dVET3s0E5q9ZCbSe+ pNUbko9F+TFkuc7XVjQPsfkDbh0I9nD0tw== -----END EC PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/ocsp_certificate.pem0000664000175100017510000000434114561227515022620 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIGYDCCBEigAwIBAgIKcjrC4hZcebbtODANBgkqhkiG9w0BAQsFADBRMQswCQYD VQQGEwJOTzEdMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIzAhBgNVBAMM GkJ1eXBhc3MgQ2xhc3MgMiBUZXN0NCBDQSA1MB4XDTE5MDUxMjE1NTgyMVoXDTE5 MTEwODIyNTkwMFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK9P b+YhJPypm4ui+AZUHPrJ6IsB9R/6Wvgec2G/GuW/UNQFktIhU10HOHAbiJeYLqNZ 1Cia8JD6NXXGbprOjIbZWvjulYTaLSlClcK0H7HZrcgrK60OeIGEtur27ga68RML hs1FG7TNyWVysifOtwW9Oo1mZQQtxViiE2Yb+Q4QqIxitnbrnFmKrVJSUHVXi8/I BK1yLrJiRBZMIw0wvAWcWEG2Gpp9PAbemlb11Zx8sm/RSGh7u60rmETbB2Pu941s XJCSQRtq5yKdtjIJTIgbe12SPkknqTqa3aUh7hgho0IymlDSeeocL60SUiUAsPEr QRWleodOR1ChXz5mFokCAwEAAaOCAokwggKFMAkGA1UdEwQCMAAwHwYDVR0jBBgw FoAUd9nQBpFm2N0ZJo1JrNowL2p7YrEwHQYDVR0OBBYEFExS23I6sLCeO6KIxzoc tr9s+HmiMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwIAYDVR0gBBkwFzALBglghEIBGgEAAgcwCAYGZ4EMAQIBMEIGA1UdHwQ7 MDkwN6A1oDOGMWh0dHA6Ly9jcmwudGVzdDQuYnV5cGFzcy5uby9jcmwvQlBDbGFz czJUNENBNS5jcmwwIQYDVR0RAQH/BBcwFYITYnV5cGFzcy5wYWNhbGlzLm5ldDB4 BggrBgEFBQcBAQRsMGowKQYIKwYBBQUHMAGGHWh0dHA6Ly9vY3NwLnRlc3Q0LmJ1 eXBhc3MuY29tMD0GCCsGAQUFBzAChjFodHRwOi8vY3J0LnRlc3Q0LmJ1eXBhc3Mu bm8vY3J0L0JQQ2xhc3MyVDRDQTUuY2VyMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDw AHYAsMyD5aX5fWuvfAnMKEkEhyrH6IsTLGNQt8b9JuFsbHcAAAFqrMQ/cQAABAMA RzBFAiEA1oWB4c6q7+tqGA4HhLNACOemr9c2aIUuWxeQE7/PlSYCIEolZ7pWVs1J VyQW/AqeuXGB7qScwUgLh9C1uOJoeRe6AHYAsMyD5aX5fWuvfAnMKEkEhyrH6IsT LGNQt8b9JuFsbHcAAAFqrMQ/cQAABAMARzBFAiAoLaNvIwMDifsDAXJBsAKHlYx7 QPLXL8onYKm8f+Sf1wIhAMepo2GX84UR7WtooqzkBZLG+PaBy1zMuUAG6mwnroF9 MA0GCSqGSIb3DQEBCwUAA4ICAQAPWLdjNS5lLL5SEtghYebtDmNj2968NYSDvb1L 1/uFwg3LCVRR1Xb3z1Hc/sc1W0IFXU0zOqEQiuP8jkVP7UqkaWuK5Eu0eP0zPI83 WBZM0+eBwxwzIMK/Q7fYKTu1+vg/FlH0WhtV43DQSik66366zvPi2Tfag9IPvRei DOjbSOBF0o4er2oCrtI0lK5YrHOdWtD7xwQIuA606P9ucuufMf+JcmduRJsVZ2Zu 3K32SMDdAnyjvQWZNbt1ex3G8vuFQEi690UBhPcha/SO8QvLS89wcaLJnyMIWdv7 54cbw+fa1nLKM7qph6Mk1yb0qpomPqLmKw4T6WX36c0vDlFSpexJLGgWDFqLUxPN qV7cJz4mi1qaYfdWXRrnyU4bl55pHTTgEzbohV7apsmytkCe1uFNrpcTh8jzAhGN PQqarX9UoESR56B/ufbBGlBWi0pkV49BFks6Ue0GVKo7djoxuV6+SsmYSE+6MNPv IUsm54TSnwxjA8WyG7pl14g1hkGFQ4NRYJMiVqK3DMABaPxVmT7NRxUQQiM0mmM7 EKNzLBeWHJF5ecdDR1MiIF3ayn+RiZb0r8aSQBMLwN1YwUZw+hSYz1eCd7bHN1gC 1ksxP61f8LBz0SwDoyOTr8wY++wqF26KfoYuKQ3LjLeHvuUtL3EMnAhiyuej8ZOZ 22spng== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/ocsp_issuer_certificate.pem0000664000175100017510000000430714561227515024214 0ustar00ericaerica22spng== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGMzCCBBugAwIBAgIJMvsa+ZFQCj8nMA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV BAYTAk5PMR0wGwYDVQQKDBRCdXlwYXNzIEFTLTk4MzE2MzMyNzEmMCQGA1UEAwwd QnV5cGFzcyBDbGFzcyAyIFRlc3Q0IFJvb3QgQ0EwHhcNMTcwMjEzMTY1MjQ2WhcN MjcwMjEzMTY1MjQ2WjBRMQswCQYDVQQGEwJOTzEdMBsGA1UECgwUQnV5cGFzcyBB Uy05ODMxNjMzMjcxIzAhBgNVBAMMGkJ1eXBhc3MgQ2xhc3MgMiBUZXN0NCBDQSA1 MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAi/vpgO2sbUQZsoxWd6us QvT/59kvw5ehoJABBXFs1J1AV1/K2hjhDXit/sNGKjzDvkfE9PJqXMnhKpPFkUzC z/NmDK++d6aRflnDvJrxlPVpp0QGbe3qOErByFjWiHoobuVItlpRO/BaBdlgGvmQ LeZFBXs/ZrLNFUKBcE+DZIyJH7vy2EB5dNNVn2mx0n+371InpKsYUaHNlxPpp+uj TOL+e4OjWTBwDaI7rVzpavozb8SPzFxjpxLLVH/j+8VPwoe3lmxr8ATyI178iRdA uxYfaKURSfu7PWjnDNTnq26E3pwW3E5zUbsADgUMh/PzoJAcszL1eHGUQaAGBP85 PlLmHr+nsPMHXOUyl7Ts6KGkZlvjnVshKwUxYAqjAC7/BY0iI0xc406NK9heeVDk NiFA8/To6mQ09vO/TBxQtkfNk2yuxiixa101peSg4/+E4VhwYv6MJxS/oVqBd2d3 wemYW/JUVeJg9wXGq1e/c09/UjGwUGwU9s5LNFEgj4v1tcvWnONzWNXkyMrs5g4e U8L/DQ3XgNrcA9zrfFq0cQhSJonj/VI/jbBYyB2yEuQAIjAN6eDIOoLmHGIIvZtE 0LL5jaZC3W518jB1OF7QSvaFtaFl0VqDy6LMXL50elMVC+hr9KpDnN0t8gaSiPyZ wEC9SMdQ7SLVOUK1Xdh3dh0CAwEAAaOCAQkwggEFMA8GA1UdEwEB/wQFMAMBAf8w HwYDVR0jBBgwFoAU0aT+MaGsc75ZynH0up0oH+tVHh4wHQYDVR0OBBYEFHfZ0AaR ZtjdGSaNSazaMC9qe2KxMA4GA1UdDwEB/wQEAwIBBjAgBgNVHSAEGTAXMAsGCWCE QgEaAQACBzAIBgZngQwBAgEwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2NybC50 ZXN0NC5idXlwYXNzLm5vL2NybC9CUENsYXNzMlQ0Um9vdENBLmNybDA5BggrBgEF BQcBAQQtMCswKQYIKwYBBQUHMAGGHWh0dHA6Ly9vY3NwLnRlc3Q0LmJ1eXBhc3Mu Y29tMA0GCSqGSIb3DQEBCwUAA4ICAQBOgxedV31NCpZQRc8yFxoqQNgBnY1UeH/h /s/9fGQzyGnTWZldEi5MGJKF6ulcYnklitlg/jic9au3xSoqP/i2smUHByX2wMrC mDpLCwio2x2p/0Wscj5asqzJE2cCWqob2iHxo36nsr3Jdd2GIlzhZ0wm8rMZxsQG FgbgHYIer79S+PIdHoZuUnCJhsJ+1PRUmm2t7vcmZpu8l4CeL0XJX98l2L8kbBds MGo1EazGAEirZnSfQKCARhUcEdavsKl067+irsGGcK4+L78Vl9S1/QPfKG30L5fv nM1X1qAdhsbjwVdrhLkjpzabT0icsW6W17HLh8UBYdA7k4GclA6h+mNrXAt7JAeZ PzMFq0I7vVJNEdolZHTVCqT0sdJiTj+phS1ztK86Wb1R/5d5B1VSb789zSdJfrwV ppXgPtZq5x3GQi6ooteWyuWj3cBcNu9TU1D8u1F0XI5gw4Y0VpxlDxysUgFQJlo4 VYmMpgr442o/35UgwzkIC7x/6dkvMZvM4jYB5JZJXjynR35XawXB/hzybermJ8BB DsY0MCOwxhpsTbyEC4wfxZ08B4JtORkToOt4OWuejovsr68Ht6ytOPj7dquoPPNM 9eGNSp94nEIiZ2n75ZMg0gIQArXU9OCV6B2TXxB7w2YB0y0teDgVhoM3IY/ltqJ/ PJrUUjM8OQ== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/ocsp_responder_certificate.pem0000664000175100017510000000320714561227515024701 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIEpjCCAo6gAwIBAgINARMIGYlEsD1LTt6D7zANBgkqhkiG9w0BAQsFADBRMQsw CQYDVQQGEwJOTzEdMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIzAhBgNV BAMMGkJ1eXBhc3MgQ2xhc3MgMiBUZXN0NCBDQSA1MB4XDTE5MDQwNTEwMDAwMFoX DTE5MDcwNDEwMDAwMFowSTELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3Mg QVMtOTgzMTYzMzI3MRswGQYDVQQDDBJCdXlwYXNzIFRlc3Q0IE9DU1AwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKGF+kYNd1fbhYT7Vf9xouZlx+4w45 Y5EowPoaSKFo4uUDDxkj4PwmMiH4w9Q2bGrCbZRrDrvlNVY/kwzLu4CIk6Ip0dgm VZGNFB3Xo9nai7rI5pn/YVvVnDIQXh1LRbekzLVyHvhRgMpRb19xN/iYsxaOJDph 8eAgbTKf6eitvfbvn/zXHj4KGKycuULI4+mwlfV3uioT4ulbT7PTVJetgi/XXFDO xMjbqx6I1ZMmzKJ6LNaFlfx6GdZsaLRDCidHzGp8Fm4ZdV+UPvMZcVDQO6rvQ3wU iGyCqgfE5e0aFvfeLoBPBtaoT0Ht1CvGdTfVet6PXrF6gh40fdEH5Ob5AgMBAAGj gYQwgYEwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBR32dAGkWbY3RkmjUms2jAvanti sTAdBgNVHQ4EFgQU3VlR+sSIVpmXklieP7IlpVUcXIowDgYDVR0PAQH/BAQDAgeA MBMGA1UdJQQMMAoGCCsGAQUFBwMJMA8GCSsGAQUFBzABBQQCBQAwDQYJKoZIhvcN AQELBQADggIBAFBRLVsBadNFAoFi0HOrfxYsiqggZGJLlgxGyi/0NBIgduG4kcpM THvplwBwMQEqyp5511pSEbLPAFj8EqC5c46hXZXmT49xlfRvr2Bo+qtTPV9szuWr 8muEIejwRrkATpqWPZWR2zVTXfB90mU2oGuRvxUVmnW4v+FrCChJo7+9yTocZJKx p4vxYfPMeggomdGAAUz94+0ppSjOLDzs3MA8uOcR0zJ2Y7UHb7PBf/HiM3GO2uKB sRgdDaGIf/PNpav0xJ/abGNNNwvXzHiMgqqImsuv/JoncPQWbClNurhXpdN7xt9C HcLX2AdggabcogjWm4guBFuFTsL1i0l8Bsu/6iPJ7ddCeANfYzf7h6AcQq12uFl3 070F29DtPh8D3FPWgRZZsxoANFjXErxfj4a4+DR+jhhkb9YM/wI0vCOM7W6PKxVn ZK5kHGOQTcQMj7RCX52gEf27M33zC7HVam+kKhGvwq7D9Bs5hZclzcbjpR4eIxT7 tzuiy5VpPh1DRLPrphPUB4xsA1dy6zbkg8OqddG6NxD++ja/iZyzSB3SeWyO02qA QoK2FzDasxpZ9rT3ioAcms3wVNe4lcd4OP8gHZONuat/gvxk6OZvAld6cnIrQZYB Tbu89ZWvhsyI3p4YC/15pUvA95j9Y0te+G+CF22Eoyb+rtz6mMletnUB -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/os-release0000664000175100017510000000027614561227515020574 0ustar00ericaericaNAME="SystemdOS" VERSION="42.42.42 LTS, Unreal" ID=systemdos ID_LIKE="something nonexistent debian" VERSION_ID="42" HOME_URL="http://www.example.com/" SUPPORT_URL="http://help.example.com/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/rsa2048_key.pem0000664000175100017510000000325414561227515021267 0ustar00ericaerica-----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDm1WIecnHjL4Fs JvxDP27GyeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop +D7s+oh0apV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3 DaNokGn7r3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf 5QU5pFx60a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDV MJ3mIB8FOW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf 3nCJFk1fAgMBAAECggEAJkhbVntagfgd+cbZbXm2sIdKQGlwXk92/Zxd3tZMcuNY rU+/C2bJ5uTEm+0R/V9f3FXlsCagGde2t7ExFnJScSRAGCuFRxudMMI/wNvUvnpR O9vN3HxrRo2rZqBkqHIZCR0d2Bxs/0cvGqTLZgsVWKV4xM07TThcE7DtvsNGegRn WFxfsRcRypkIvZoba1HagvCituRBEa07R7mQp8kRhP9ZeRq3bZws9qBmqzj1cylG q8QA4Foq7sK8P78bpIhrcOFBDAr+Vr1ZGY6u01J0w13MUtl6iIx4VCjQKt4NkzsK dj2q+GAMwhReR2ZS42o8LiyGpwusj+dKIFfFekgK2QKBgQD4wwmRDgvt85brQTNF Tkhui0eToz5oXt8mVDb58nwkpojFQOv87ZyNsEqm7S0t/3RtEViVio2aymTMsrz4 21vRq46dvhINQ3DoMok6xIchEOEgMeonOilkURWtrMjD/Kn297Asv7zOqI5BCNiP 3FFcRqf+CaqbhnOgMkcI5z6b7QKBgQDtjM1otFFHyS7ctyLRuMeFyxWUSbWHvi8U xjUW256c6wpQ2DBLSVB61VQjfrSjkZ5DJVFGnbw42HxSDafL11mzTbY1vDbgtgLK YiuVHG7OYZJTLaZoM68BseX4xHN8FztnvvP1ttuk5oFb+vD8q6ODZSEawRd3PvtX D7RtNouc+wKBgQDiwBWGTUF+gt18T5BGilbnvLlf0Btg06mgrH74UpnqZoqhEs6J XKWpWZqSkfruxL4BdSBEH2l4QSiklgA+7uTBOBnlm42k3WaboQUJtn5eG5651AXV /+Qe9vJFvwu56iObZKcIAzY9QdN5YHDWoULgU99pZrJG1cWrrmilqvOc+QKBgQCB iOslslY0N+926eJxzDn4qkJtJzh2+e1AfcjLWx0F4mEwroK/Ow5IvPVxmZE1NJ3B baMBR9gwg1RfhhS+4gKG9NRsPuMJ7BZfd+LeH7AImEorU1RPtAc1fGW0HqP+wchi DU2I6pqhNBTMLG2myo2Sg93mce6y1sRFuEmh2EGPawKBgQC3uUEdjQekXaxXfYHi 1Dk3Ht1a9t8XxwoCVRqicE7lqlwDtS2y9lHAeUP7JNy8ZGNjx8srRZpkYVMztugo Ecw26UA7FbNqJP5OPkGjfiFqtOq70h9vlfLdiAPmoqyOx//RkgiNXt9m5xcDzzdB 7EtBK59KSiQkB8fHtooy7Ipiiw== -----END PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/rsa256_key.pem0000664000175100017510000000045214561227515021203 0ustar00ericaerica-----BEGIN RSA PRIVATE KEY----- MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/rsa512_key.pem0000664000175100017510000000075514561227515021204 0ustar00ericaerica-----BEGIN RSA PRIVATE KEY----- MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79 vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj 8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq 6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/ -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3390837 certbot-2.9.0/certbot/tests/testdata/sample-archive/0000775000175100017510000000000014561227516021506 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-archive/cert1.pem0000664000175100017510000000331014561227515023223 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5 MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+ slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8 L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0 c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB 8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0 b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2 Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU +ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-archive/chain1.pem0000664000175100017510000000214314561227515023353 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i 8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8 tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj 7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8 BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7 eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW rFo4Uv1EnkKJm3vJFe50eJGhEKlx -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-archive/fullchain1.pem0000664000175100017510000000545314561227515024245 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5 MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+ slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8 L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0 c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB 8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0 b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2 Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU +ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i 8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8 tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj 7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8 BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7 eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW rFo4Uv1EnkKJm3vJFe50eJGhEKlx -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-archive/privkey1.pem0000664000175100017510000000325014561227515023762 0ustar00ericaerica-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8rnaiynCHVmeV WitX7oZRA22bb7NhPrJZuHZkktZxb3uzHZWevStIHbZ4ZIj0NMBkE5YoHg2fH2PI i/OxmTtLaeX+0vkCTTWAomMOo+KqeSQcv8xttpCc9ULru/71zGC8y7yusemIsYlg Fsqqr5VFQS0ta+Ft4QOWfhdRCLF3ss+0yXt5QMyFgAMaXx/Tr4iM9qxA6LbgnVO+ 9VEiyzUsOKiaIzZU8VKWRupRhbPChm/LakTl5J2jChOD7zd0iE4sUlExW5LbdVs+ tJqFmzXSzCr4JM7lD6+L4jtj+EjubJTQGNGXhLLD6VQHpTdxL6wHBGbaCphuX50A 1aPLlWoNAgMBAAECggEAfKKWFWS6PnwSAnNErFoQeZVVItb/XB5JO8EA2+CvLNFi mefR/MCixYlzDkYCvaXW7ISPrMJlZxYaGNBx0oAQzfkPB2wfNqj/zY/29SXGxast 8puzk0mEb1oHsaZGfeFaiXvfkFpPlI8J2uJTT7qaVNv/1sArciSv9QonpsyiRhlB yqT49juNVoR1tJHyXzkkRfHKTG8OlJd4kuFOl3fM9dTFPQ/ft0kTNAQ/B4SFvSwF RJsbLbsbFGsUdV9ekE6UX6oWD/Ah707rvgtCyS0Bc+0O3t2EKwmm3RXPRUMHCVxE bKdTxRB4etbjMVXMuVhB8Y4GbfrtMCy+qxZQ6znCAQKBgQDr7bcYAZVZp/nBMVB+ lBO9w73J6lnEWm6bZ9728KlGAKETaRhxZQSi6TN6MWwNwnk6rinyz4uVwVr9ZRCs WkB1TbvW0JNcWdr3YClwsKXAt8X22bjGe0LagDJHG6r1TPS+MdovOS2M6IMaxlbT rzFhSJ8ojLX3tqnOsmc7YAFLjQKBgQDMu8E9hoJt82lQzOGrjHmGzGEu2GLx9WKO e4nkj335kX6fIhMMqSXBFbTJZwXoYvk5J8ZnaARbYG0m5nxDCwRjX5HWa8q0B2Po ta53w01sKKznzlPjUhsdhEthun7MCFfLZpgvcZ9xVzOXo3/Zfn2+RrsPSjrVDqBy hj+k5mW4gQKBgHFWKf3LTO7cBdvsD8ou4mjn7nVgMi1kb/wR4wdnxzmMtdR4STi4 GYkVVBhgQ5M8mDY7UoWFdH3FfCt8cI0Lcimn5ROl8RSNSeZKeL3c7lNtNRmHr/8R WaVTrlOAlBjxFiWEF1dWNW6ah9jF7RIV+DfOxj6ZkhTk2CAmjfb1AMpFAoGABf96 KdNG/vGipDtcYSo8ZTaXoke0nmISARqdb5TEnAsnKoJVDInoEUARi9T411YO9x2z MlRZzFOG3xzhhxVLi53BKAcAaUXOJ4MrGVcfbYvDhQcGbiJ5qOO3UaWlEVUtPUhE LR+nDCsB1+9yT2zlQi3QTSJflt5W1QQZ2TrmwAECgYEAvQ7+sTcHs1K9yKj7koEu A19FbMA0IwvrVRcV/VqmlsoW6e6wW2YND+GtaDbKdD0aBPivqLJwpNFrsRA+W0iB vzmML6sKhhL+j7tjSgq+iQdBkKz0j9PyReuhe9CRnljMmyun+4qKEk0KUvxBrjPY Skn+ML18qyUoEPnmbpfHxCs= -----END PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3390837 certbot-2.9.0/certbot/tests/testdata/sample-archive-ec/0000775000175100017510000000000014561227516022073 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-archive-ec/cert1.pem0000664000175100017510000000203214561227515023610 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIC2zCCAcOgAwIBAgIIBvrEnbPRYu8wDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjEwNzQw WhcNMjUxMDEyMjEwNzQwWjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARjMhuW0ENPPC33PjB5XsYU CRw640kPQENIDatcTJaENZIZdqKd6rI6jc+lpbmXot7Zi52clJlSJS+V6oDAt2Lh o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUj7Kd3ENqxlPf8B2bIGhsjydX mPswHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQCl k0JXsa8y7fg41WWMDhw60bPW77O0FtOmTcnhdI5daYNemQVk+Q5EMaBLQ/oGjgXd 9QXFzXH1PL904YEnSLt+iTpXn++7rQSNzQsdYqw0neWk4f5pEBiN+WORpb6mwobV ifMtBOkNEHvrJ2Pkci9U1lLwtKD/DSew6QtJU5DSkmH1XdGuMJiubygEIvELtvgq cP9S368ZvPmPGmKaJQXBiuaR8MTjY/Bkr79aXQMjKbf+mpn7h0POCcePk1DY/rm6 Da+X16lf0hHyQhSUa7Vgyim6rK1/hlw+Z00i+sQCKD9Ih7kXuuGqfSDC33cfO8Tj o/MXO8lcxkrem5zU5QWP -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-archive-ec/chain1.pem0000664000175100017510000000227014561227515023741 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5 CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG 9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-archive-ec/fullchain1.pem0000664000175100017510000000432214561227515024624 0ustar00ericaerica-----BEGIN CERTIFICATE----- MIIC2zCCAcOgAwIBAgIILlmGtZhUFEwwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjA1MDM0 WhcNMjUxMDEyMjA1MDM0WjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARHEzR8JPWrEmpmgM+F2bk5 9mT0u6CjzmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/ o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU1CsVL+bPnzaxxQ5jUENmQJIO lKwwHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBn 2D8loC7pfk28JYpFLr5lmFKJWWmtLGlpsWDj61fVjtTfGKLziJz+MM6il4Y3hIz5 58qiFK0ue0M63dIBJ33N+XxSEXon4Q0gy/zRWfH9jtPJ3FwfjkU/RT9PAUClYi0G ptNWnTmgQkNzousbcAtRNXuuShH3856vhUnwkX+xM+cbIDi1JVmFjcGrEEQJ0rUF mv2ZTyfbWbUs3v4rReETi2NVzr1Ql6J+ByNcMvHODzFy3t0L6yelAw2ca1I+c9HU +Z0tnp/ykR7eXNuVLivok8UBf5OC413lh8ZO5g+Bgzh/LdtkUuavg1MYtEX0H6mX 9U7y3nVI8WEbPGf+HDeu -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5 CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG 9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-archive-ec/privkey1.pem0000664000175100017510000000036114561227515024347 0ustar00ericaerica-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNgefv2dad4U1VYEi 0WkdHuqywi5QXAe30OwNTTGjhbihRANCAARHEzR8JPWrEmpmgM+F2bk59mT0u6Cj zmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/ -----END PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-renewal-ancient.conf0000664000175100017510000000352514561227515024014 0ustar00ericaericacert = MAGICDIR/live/sample-renewal-ancient/cert.pem privkey = MAGICDIR/live/sample-renewal-ancient/privkey.pem chain = MAGICDIR/live/sample-renewal-ancient/chain.pem fullchain = MAGICDIR/live/sample-renewal-ancient/fullchain.pem renew_before_expiry = 1 year # Options and defaults used in the renewal process [renewalparams] no_self_upgrade = False apache_enmod = a2enmod no_verify_ssl = False ifaces = None apache_dismod = a2dismod register_unsafely_without_email = False apache_handle_modules = True uir = None installer = None nginx_ctl = nginx config_dir = MAGICDIR text_mode = False func = staging = True prepare = False work_dir = /var/lib/letsencrypt tos = False init = False http01_port = 80 duplicate = False noninteractive_mode = True key_path = None nginx = False nginx_server_root = /etc/nginx fullchain_path = /home/ubuntu/letsencrypt/chain.pem email = None csr = None agree_dev_preview = None redirect = None verb = certonly verbose_count = -3 config_file = None renew_by_default = False hsts = False apache_handle_sites = True authenticator = webroot domains = isnot.org, rsa_key_size = 2048 apache_challenge_location = /etc/apache2 checkpoints = 1 manual_test_mode = False apache = False cert_path = /home/ubuntu/letsencrypt/cert.pem webroot_path = /var/www/ reinstall = False expand = False strict_permissions = False apache_server_root = /etc/apache2 account = None dry_run = False manual_public_ip_logging_ok = False chain_path = /home/ubuntu/letsencrypt/chain.pem break_my_certs = False standalone = True manual = False server = https://acme-staging.api.letsencrypt.org/directory webroot = True os_packages_only = False apache_init_script = None user_agent = None apache_le_vhost_ext = -le-ssl.conf debug = False logs_dir = /var/log/letsencrypt apache_vhost_root = /etc/apache2/sites-available configurator = None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-renewal-deprecated-option.conf0000664000175100017510000000105114561227515025771 0ustar00ericaerica# renew_before_expiry = 30 days version = 1.11.0 archive_dir = MAGICDIR/live/sample-renewal-deprecated-option cert = MAGICDIR/live/sample-renewal-deprecated-option/cert.pem privkey = MAGICDIR/live/sample-renewal-deprecated-option/privkey.pem chain = MAGICDIR/live/sample-renewal-deprecated-option/chain.pem fullchain = MAGICDIR/live/sample-renewal-deprecated-option/fullchain.pem # Options used in the renewal process [renewalparams] account = ffffffffffffffffffffffffffffffff authenticator = nginx installer = nginx manual_public_ip_logging_ok = None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-renewal-ec.conf0000664000175100017510000000366214561227515022764 0ustar00ericaerica# add some stuff here # assets/integration_tests cert = MAGICDIR/live/sample-renewal-ec/cert.pem privkey = MAGICDIR/live/sample-renewal-ec/privkey.pem chain = MAGICDIR/live/sample-renewal-ec/chain.pem fullchain = MAGICDIR/live/sample-renewal-ec/fullchain.pem renew_before_expiry = 4 years # Options and defaults used in the renewal process [renewalparams] no_self_upgrade = False apache_enmod = a2enmod no_verify_ssl = False ifaces = None apache_dismod = a2dismod register_unsafely_without_email = False apache_handle_modules = True uir = None installer = None nginx_ctl = nginx config_dir = MAGICDIR text_mode = False func = staging = True prepare = False work_dir = /var/lib/letsencrypt tos = False init = False http01_port = 80 duplicate = False noninteractive_mode = True key_path = None nginx = False nginx_server_root = /etc/nginx fullchain_path = /home/ubuntu/letsencrypt/chain.pem email = None csr = None agree_dev_preview = None redirect = None verb = certonly verbose_count = -3 config_file = None renew_by_default = False hsts = False apache_handle_sites = True authenticator = standalone domains = isnot.org, key_type = ecdsa elliptic_curve = secp256r1 apache_challenge_location = /etc/apache2 checkpoints = 1 manual_test_mode = False apache = False cert_path = /home/ubuntu/letsencrypt/cert.pem webroot_path = None reinstall = False expand = False strict_permissions = False apache_server_root = /etc/apache2 account = None dry_run = False manual_public_ip_logging_ok = False chain_path = /home/ubuntu/letsencrypt/chain.pem break_my_certs = False standalone = True manual = False server = https://acme-staging-v02.api.letsencrypt.org/directory webroot = False os_packages_only = False apache_init_script = None user_agent = None apache_le_vhost_ext = -le-ssl.conf debug = False logs_dir = /var/log/letsencrypt apache_vhost_root = /etc/apache2/sites-available configurator = None must_staple = True [[webroot_map]] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/sample-renewal.conf0000664000175100017510000000356714561227515022403 0ustar00ericaericacert = MAGICDIR/live/sample-renewal/cert.pem privkey = MAGICDIR/live/sample-renewal/privkey.pem chain = MAGICDIR/live/sample-renewal/chain.pem fullchain = MAGICDIR/live/sample-renewal/fullchain.pem renew_before_expiry = 4 years # Options and defaults used in the renewal process [renewalparams] no_self_upgrade = False apache_enmod = a2enmod no_verify_ssl = False ifaces = None apache_dismod = a2dismod register_unsafely_without_email = False apache_handle_modules = True uir = None installer = None nginx_ctl = nginx config_dir = MAGICDIR text_mode = False func = staging = True prepare = False work_dir = /var/lib/letsencrypt tos = False init = False http01_port = 80 duplicate = False noninteractive_mode = True key_path = None nginx = False nginx_server_root = /etc/nginx fullchain_path = /home/ubuntu/letsencrypt/chain.pem email = None csr = None agree_dev_preview = None redirect = None verb = certonly verbose_count = -3 config_file = None renew_by_default = False hsts = False apache_handle_sites = True authenticator = standalone domains = isnot.org, rsa_key_size = 2048 elliptic_curve = secp256r1 apache_challenge_location = /etc/apache2 checkpoints = 1 manual_test_mode = False apache = False cert_path = /home/ubuntu/letsencrypt/cert.pem webroot_path = None reinstall = False expand = False strict_permissions = False apache_server_root = /etc/apache2 account = None dry_run = False manual_public_ip_logging_ok = False chain_path = /home/ubuntu/letsencrypt/chain.pem break_my_certs = False standalone = True manual = False server = https://acme-staging-v02.api.letsencrypt.org/directory webroot = False os_packages_only = False apache_init_script = None user_agent = None apache_le_vhost_ext = -le-ssl.conf debug = False logs_dir = /var/log/letsencrypt apache_vhost_root = /etc/apache2/sites-available configurator = None must_staple = True [[webroot_map]] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/testdata/webrootconftest.ini0000664000175100017510000000006614561227515022537 0ustar00ericaericawebroot webroot-path = /tmp domains = eg.com, eg2.com ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/tests/util.py0000664000175100017510000004336614561227515016337 0ustar00ericaerica"""Test utilities.""" import atexit from contextlib import ExitStack import copy from importlib import reload as reload_module import io import logging import multiprocessing from multiprocessing import synchronize import shutil import sys import tempfile from typing import Any from typing import Callable from typing import cast from typing import IO from typing import Iterable from typing import List from typing import Optional from typing import Union import unittest from unittest import mock from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey import josepy as jose from OpenSSL import crypto from certbot import configuration from certbot import util from certbot._internal import constants from certbot._internal import lock from certbot._internal import storage from certbot._internal.display import obj as display_obj from certbot.compat import filesystem from certbot.compat import os from certbot.display import util as display_util from certbot.plugins import common if sys.version_info >= (3, 9): # pragma: no cover import importlib.resources as importlib_resources else: # pragma: no cover import importlib_resources class DummyInstaller(common.Installer): """Dummy installer plugin for test purpose.""" def get_all_names(self) -> Iterable[str]: return [] def deploy_cert(self, domain: str, cert_path: str, key_path: str, chain_path: str, fullchain_path: str) -> None: pass def enhance(self, domain: str, enhancement: str, options: Optional[Union[List[str], str]] = None) -> None: pass def supported_enhancements(self) -> List[str]: return [] def save(self, title: Optional[str] = None, temporary: bool = False) -> None: pass def config_test(self) -> None: pass def restart(self) -> None: pass @classmethod def add_parser_arguments(cls, add: Callable[..., None]) -> None: pass def prepare(self) -> None: pass def more_info(self) -> str: return "" def vector_path(*names: str) -> str: """Path to a test vector.""" _file_manager = ExitStack() atexit.register(_file_manager.close) vector_ref = importlib_resources.files(__package__).joinpath('testdata', *names) path = _file_manager.enter_context(importlib_resources.as_file(vector_ref)) return str(path) def load_vector(*names: str) -> bytes: """Load contents of a test vector.""" vector_ref = importlib_resources.files(__package__).joinpath('testdata', *names) data = vector_ref.read_bytes() # Try at most to convert CRLF to LF when data is text try: return data.decode().replace('\r\n', '\n').encode() except ValueError: # Failed to process the file with standard encoding. # Most likely not a text file, return its bytes untouched. return data def _guess_loader(filename: str, loader_pem: int, loader_der: int) -> int: _, ext = os.path.splitext(filename) if ext.lower() == '.pem': return loader_pem elif ext.lower() == '.der': return loader_der raise ValueError("Loader could not be recognized based on extension") # pragma: no cover def load_cert(*names: str) -> crypto.X509: """Load certificate.""" loader = _guess_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_certificate(loader, load_vector(*names)) def load_csr(*names: str) -> crypto.X509Req: """Load certificate request.""" loader = _guess_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_certificate_request(loader, load_vector(*names)) def load_comparable_csr(*names: str) -> jose.ComparableX509: """Load ComparableX509 certificate request.""" return jose.ComparableX509(load_csr(*names)) def load_rsa_private_key(*names: str) -> jose.ComparableRSAKey: """Load RSA private key.""" loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) loader_fn: Callable[..., Any] if loader == crypto.FILETYPE_PEM: loader_fn = serialization.load_pem_private_key else: loader_fn = serialization.load_der_private_key return jose.ComparableRSAKey( cast(RSAPrivateKey, loader_fn(load_vector(*names), password=None, backend=default_backend()))) def load_pyopenssl_private_key(*names: str) -> crypto.PKey: """Load pyOpenSSL private key.""" loader = _guess_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_privatekey(loader, load_vector(*names)) def make_lineage(config_dir: str, testfile: str, ec: bool = True) -> str: """Creates a lineage defined by testfile. This creates the archive, live, and renewal directories if necessary and creates a simple lineage. :param str config_dir: path to the configuration directory :param str testfile: configuration file to base the lineage on :param bool ec: True if we generate the lineage with an ECDSA key :returns: path to the renewal conf file for the created lineage :rtype: str """ lineage_name = testfile[:-len('.conf')] conf_dir = os.path.join( config_dir, constants.RENEWAL_CONFIGS_DIR) archive_dir = os.path.join( config_dir, constants.ARCHIVE_DIR, lineage_name) live_dir = os.path.join( config_dir, constants.LIVE_DIR, lineage_name) for directory in (archive_dir, conf_dir, live_dir,): if not os.path.exists(directory): filesystem.makedirs(directory) sample_archive = vector_path('sample-archive{}'.format('-ec' if ec else '')) for kind in os.listdir(sample_archive): shutil.copyfile(os.path.join(sample_archive, kind), os.path.join(archive_dir, kind)) for kind in storage.ALL_FOUR: os.symlink(os.path.join(archive_dir, '{0}1.pem'.format(kind)), os.path.join(live_dir, '{0}.pem'.format(kind))) conf_path = os.path.join(config_dir, conf_dir, testfile) with open(vector_path(testfile)) as src: with open(conf_path, 'w') as dst: dst.writelines( line.replace('MAGICDIR', config_dir) for line in src) return conf_path def patch_display_util() -> mock.MagicMock: """Patch certbot.display.util to use a special mock display utility. The mock display utility works like a regular mock object, except it also also asserts that methods are called with valid arguments. The mock created by this patch mocks out Certbot internals. That is, the mock object will be called by the certbot.display.util functions and the mock returned by that call will be used as the display utility. This was done to simplify the transition from zope.component and mocking certbot.display.util functions directly in test code should be preferred over using this function in the future. See https://github.com/certbot/certbot/issues/8948 :returns: patch on the function used internally by certbot.display.util to get a display utility instance :rtype: mock.MagicMock """ return cast(mock.MagicMock, mock.patch('certbot._internal.display.obj.get_display', new_callable=_create_display_util_mock)) def patch_display_util_with_stdout( stdout: Optional[IO] = None) -> mock.MagicMock: """Patch certbot.display.util to use a special mock display utility. The mock display utility works like a regular mock object, except it also asserts that methods are called with valid arguments. The mock created by this patch mocks out Certbot internals. That is, the mock object will be called by the certbot.display.util functions and the mock returned by that call will be used as the display utility. This was done to simplify the transition from zope.component and mocking certbot.display.util functions directly in test code should be preferred over using this function in the future. See https://github.com/certbot/certbot/issues/8948 The `message` argument passed to the display utility methods is passed to stdout's write method. :param object stdout: object to write standard output to; it is expected to have a `write` method :returns: patch on the function used internally by certbot.display.util to get a display utility instance :rtype: mock.MagicMock """ stdout = stdout if stdout else io.StringIO() return cast(mock.MagicMock, mock.patch('certbot._internal.display.obj.get_display', new=_create_display_util_mock_with_stdout(stdout))) class FreezableMock: """Mock object with the ability to freeze attributes. This class works like a regular mock.MagicMock object, except attributes and behavior set before the object is frozen cannot be changed during tests. If a func argument is provided to the constructor, this function is called first when an instance of FreezableMock is called, followed by the usual behavior defined by MagicMock. The return value of func is ignored. """ def __init__(self, frozen: bool = False, func: Optional[Callable[..., Any]] = None, return_value: Any = mock.sentinel.DEFAULT) -> None: self._frozen_set = set() if frozen else {'freeze', } self._func = func self._mock = mock.MagicMock() if return_value != mock.sentinel.DEFAULT: self.return_value = return_value self._frozen = frozen def freeze(self) -> None: """Freeze object preventing further changes.""" self._frozen = True def __call__(self, *args: Any, **kwargs: Any) -> mock.MagicMock: if self._func is not None: self._func(*args, **kwargs) return self._mock(*args, **kwargs) def __getattribute__(self, name: str) -> Any: if name == '_frozen': try: return object.__getattribute__(self, name) except AttributeError: return False elif name in ('return_value', 'side_effect',): return getattr(object.__getattribute__(self, '_mock'), name) elif name == '_frozen_set' or name in self._frozen_set: return object.__getattribute__(self, name) else: return getattr(object.__getattribute__(self, '_mock'), name) def __setattr__(self, name: str, value: Any) -> None: """ Before it is frozen, attributes are set on the FreezableMock instance and added to the _frozen_set. Attributes in the _frozen_set cannot be changed after the FreezableMock is frozen. In this case, they are set on the underlying _mock. In cases of return_value and side_effect, these attributes are always passed through to the instance's _mock and added to the _frozen_set before the object is frozen. """ if self._frozen: if name in self._frozen_set: raise AttributeError('Cannot change frozen attribute ' + name) return setattr(self._mock, name, value) if name != '_frozen_set': self._frozen_set.add(name) if name in ('return_value', 'side_effect'): return setattr(self._mock, name, value) return object.__setattr__(self, name, value) def _create_display_util_mock() -> FreezableMock: display = FreezableMock() # Use pylint code for disable to keep on single line under line length limit method_list = [func for func in dir(display_obj.FileDisplay) if callable(getattr(display_obj.FileDisplay, func)) and not func.startswith("__")] for method in method_list: if method != 'notification': frozen_mock = FreezableMock(frozen=True, func=_assert_valid_call) setattr(display, method, frozen_mock) display.freeze() return FreezableMock(frozen=True, return_value=display) def _create_display_util_mock_with_stdout(stdout: IO) -> FreezableMock: def _write_msg(message: str, *unused_args: Any, **unused_kwargs: Any) -> None: """Write to message to stdout. """ if message: stdout.write(message) def mock_method(*args: Any, **kwargs: Any) -> None: """ Mock function for display utility methods. """ _assert_valid_call(args, kwargs) _write_msg(*args, **kwargs) display = FreezableMock() # Use pylint code for disable to keep on single line under line length limit method_list = [func for func in dir(display_obj.FileDisplay) if callable(getattr(display_obj.FileDisplay, func)) and not func.startswith("__")] for method in method_list: if method == 'notification': frozen_mock = FreezableMock(frozen=True, func=_write_msg) else: frozen_mock = FreezableMock(frozen=True, func=mock_method) setattr(display, method, frozen_mock) display.freeze() return FreezableMock(frozen=True, return_value=display) def _assert_valid_call(*args: Any, **kwargs: Any) -> None: assert_args = [args[0] if args else kwargs['message']] assert_kwargs = { 'default': kwargs.get('default', None), 'cli_flag': kwargs.get('cli_flag', None), 'force_interactive': kwargs.get('force_interactive', False), } display_util.assert_valid_call(*assert_args, **assert_kwargs) class TempDirTestCase(unittest.TestCase): """Base test class which sets up and tears down a temporary directory""" def setUp(self) -> None: """Execute before test""" self.tempdir = tempfile.mkdtemp() def tearDown(self) -> None: """Execute after test""" # Cleanup opened resources after a test. This is usually done through atexit handlers in # Certbot, but during tests, atexit will not run registered functions before tearDown is # called and instead will run them right before the entire test process exits. # It is a problem on Windows, that does not accept to clean resources before closing them. logging.shutdown() # Remove logging handlers that have been closed so they won't be # accidentally used in future tests. logging.getLogger().handlers = [] util._release_locks() # pylint: disable=protected-access shutil.rmtree(self.tempdir) class ConfigTestCase(TempDirTestCase): """Test class which sets up a NamespaceConfig object.""" def setUp(self) -> None: super().setUp() self.config = configuration.NamespaceConfig( # We make a copy here so any mutable values from CLI_DEFAULTS do not get modified. mock.MagicMock(**copy.deepcopy(constants.CLI_DEFAULTS)), ) self.config.set_argument_sources({}) self.config.namespace.verb = "certonly" self.config.namespace.config_dir = os.path.join(self.tempdir, 'config') self.config.namespace.work_dir = os.path.join(self.tempdir, 'work') self.config.namespace.logs_dir = os.path.join(self.tempdir, 'logs') self.config.namespace.cert_path = constants.CLI_DEFAULTS['auth_cert_path'] self.config.namespace.fullchain_path = constants.CLI_DEFAULTS['auth_chain_path'] self.config.namespace.chain_path = constants.CLI_DEFAULTS['auth_chain_path'] self.config.namespace.server = "https://example.com" def _handle_lock(event_in: synchronize.Event, event_out: synchronize.Event, path: str) -> None: """ Acquire a file lock on given path, then wait to release it. This worker is coordinated using events to signal when the lock should be acquired and released. :param multiprocessing.Event event_in: event object to signal when to release the lock :param multiprocessing.Event event_out: event object to signal when the lock is acquired :param path: the path to lock """ if os.path.isdir(path): my_lock = lock.lock_dir(path) else: my_lock = lock.LockFile(path) try: event_out.set() assert event_in.wait(timeout=20), 'Timeout while waiting to release the lock.' finally: my_lock.release() def lock_and_call(callback: Callable[[], Any], path_to_lock: str) -> None: """ Grab a lock on path_to_lock from a foreign process then execute the callback. :param callable callback: object to call after acquiring the lock :param str path_to_lock: path to file or directory to lock """ # Reload certbot.util module to reset internal _LOCKS dictionary. reload_module(util) emit_event = multiprocessing.Event() receive_event = multiprocessing.Event() process = multiprocessing.Process(target=_handle_lock, args=(emit_event, receive_event, path_to_lock)) process.start() # Wait confirmation that lock is acquired assert receive_event.wait(timeout=10), 'Timeout while waiting to acquire the lock.' # Execute the callback callback() # Trigger unlock from foreign process emit_event.set() # Wait for process termination process.join(timeout=10) assert process.exitcode == 0 def skip_on_windows(reason: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to skip permanently a test on Windows. A reason is required.""" def wrapper(function: Callable[..., Any]) -> Callable[..., Any]: """Wrapped version""" return unittest.skipIf(sys.platform == 'win32', reason)(function) return wrapper def temp_join(path: str) -> str: """ Return the given path joined to the tempdir path for the current platform Eg.: 'cert' => /tmp/cert (Linux) or 'C:\\Users\\currentuser\\AppData\\Temp\\cert' (Windows) """ return os.path.join(tempfile.gettempdir(), path) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/certbot/util.py0000664000175100017510000006211214561227515015163 0ustar00ericaerica"""Utilities for all Certbot.""" import argparse import atexit import errno import itertools import logging import platform import re import socket import subprocess import sys from typing import Any from typing import Callable from typing import Dict from typing import IO from typing import List from typing import NamedTuple from typing import Optional from typing import Set from typing import Tuple from typing import Union import configargparse from certbot import errors from certbot._internal import constants from certbot._internal import lock from certbot.compat import filesystem from certbot.compat import os _USE_DISTRO = sys.platform.startswith('linux') if _USE_DISTRO: import distro logger = logging.getLogger(__name__) class Key(NamedTuple): """Container for an optional file path and contents for a PEM-formated private key.""" file: Optional[str] pem: bytes class CSR(NamedTuple): """Container for an optional file path and contents for a PEM or DER-formatted CSR.""" file: Optional[str] data: bytes # Note: form is the type of data, "pem" or "der" form: str class LooseVersion: """A version with loose rules, i.e. any given string is a valid version number. but regular comparison is not supported. Instead, the `try_risky_comparison` method is provided, which may return an error if two LooseVersions are 'incomparible'. For example when integer and string version components are present in the same position. Differences with old distutils.version.LooseVersion: (https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/version.py#L269) Most version comparisons should give the same result. However, if a version has multiple trailing zeroes, not all of them are used in the comparison. This ensure that, for example, "2.0" and "2.0.0" are equal. """ def __init__(self, version_string: str) -> None: """Parses a version string into its components. :param str version_string: version string """ components: List[Union[int, str]] components = [x for x in _VERSION_COMPONENT_RE.split(version_string) if x and x != '.'] for i, obj in enumerate(components): try: components[i] = int(obj) except ValueError: pass self.version_components = components def try_risky_comparison(self, other: 'LooseVersion') -> int: """Compares the LooseVersion to another value. If the other value is another LooseVersion, the version components are compared. Otherwise, an exception is raised. Comparison is performed element-wise. If the version components being compared are of different types, the two versions are considered incomparible. Otherwise, if either of the components is not equal to the other, less or greater is returned based on the comparison's result. In case the two versions are of different lengths, some elements in the longer version have not yet been compared. If these are all equal to zero, the two versions are equal. Otherwise, the longer version is greater. If the two versions are incomparible, an exception is raised. Otherwise, the returned integer indicates the result of the comparison. If self == other, 0 is returned. If self > other, 1 is returned. If self < other -1 is returned. Examples: Equality: - LooseVersion('1.0').try_risky_comparison(LooseVersion('1.0')) -> 0 - LooseVersion('2.0.0a').try_risky_comparison(LooseVersion('2.0.0a')) -> 0 Inequality: - LooseVersion('2.0.0').try_risky_comparison(LooseVersion('1.0')) -> 1 - LooseVersion('1.0.1').try_risky_comparison(LooseVersion('2.0a')) -> -1 Incomparability: - LooseVersion('1a').try_risky_comparison(LooseVersion('1.0')) -> ValueError """ try: for self_vc, other_vc in itertools.zip_longest(self.version_components, other.version_components, fillvalue=0): # ensure mypy ignores types here and catch any TypeErrors if self_vc < other_vc: # type: ignore return -1 elif self_vc > other_vc: # type: ignore return 1 return 0 except TypeError: raise ValueError("Cannot meaningfully compare LooseVersion {} with LooseVersion {} " "due to comparison of version components with different types." .format(self.version_components, other.version_components)) # ANSI SGR escape codes # Formats text as bold or with increased intensity ANSI_SGR_BOLD = '\033[1m' # Colors text red ANSI_SGR_RED = "\033[31m" # Resets output format ANSI_SGR_RESET = "\033[0m" PERM_ERR_FMT = os.linesep.join(( "The following error was encountered:", "{0}", "Either run as root, or set --config-dir, " "--work-dir, and --logs-dir to writeable paths.")) # Stores importing process ID to be used by atexit_register() _INITIAL_PID = os.getpid() # Maps paths to locked directories to their lock object. All locks in # the dict are attempted to be cleaned up at program exit. If the # program exits before the lock is cleaned up, it is automatically # released, but the file isn't deleted. _LOCKS: Dict[str, lock.LockFile] = {} _VERSION_COMPONENT_RE = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) def env_no_snap_for_external_calls() -> Dict[str, str]: """ When Certbot is run inside a Snap, certain environment variables are modified. But Certbot sometimes calls out to external programs, since it uses classic confinement. When we do that, we must modify the env to remove our modifications so it will use the system's libraries, since they may be incompatible with the versions of libraries included in the Snap. For example, apachectl, Nginx, and anything run from inside a hook should call this function and pass the results into the ``env`` argument of ``subprocess.Popen``. :returns: A modified copy of os.environ ready to pass to Popen :rtype: dict """ env = os.environ.copy() # Avoid accidentally modifying env if 'SNAP' not in env or 'CERTBOT_SNAPPED' not in env: return env for path_name in ('PATH', 'LD_LIBRARY_PATH'): if path_name in env: env[path_name] = ':'.join(x for x in env[path_name].split(':') if env['SNAP'] not in x) return env def run_script(params: List[str], log: Callable[[str], None]=logger.error) -> Tuple[str, str]: """Run the script with the given params. :param list params: List of parameters to pass to subprocess.run :param callable log: Logger method to use for errors """ try: proc = subprocess.run(params, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, env=env_no_snap_for_external_calls()) except (OSError, ValueError): msg = "Unable to run the command: %s" % " ".join(params) log(msg) raise errors.SubprocessError(msg) if proc.returncode != 0: msg = "Error while running %s.\n%s\n%s" % ( " ".join(params), proc.stdout, proc.stderr) # Enter recovery routine... log(msg) raise errors.SubprocessError(msg) return proc.stdout, proc.stderr def exe_exists(exe: str) -> bool: """Determine whether path/name refers to an executable. :param str exe: Executable path or name :returns: If exe is a valid executable :rtype: bool """ path, _ = os.path.split(exe) if path: return filesystem.is_executable(exe) for path in os.environ["PATH"].split(os.pathsep): if filesystem.is_executable(os.path.join(path, exe)): return True return False def lock_dir_until_exit(dir_path: str) -> None: """Lock the directory at dir_path until program exit. :param str dir_path: path to directory :raises errors.LockError: if the lock is held by another process """ if not _LOCKS: # this is the first lock to be released at exit atexit_register(_release_locks) if dir_path not in _LOCKS: _LOCKS[dir_path] = lock.lock_dir(dir_path) def _release_locks() -> None: for dir_lock in _LOCKS.values(): try: dir_lock.release() except: # pylint: disable=bare-except msg = 'Exception occurred releasing lock: {0!r}'.format(dir_lock) logger.debug(msg, exc_info=True) _LOCKS.clear() def set_up_core_dir(directory: str, mode: int, strict: bool) -> None: """Ensure directory exists with proper permissions and is locked. :param str directory: Path to a directory. :param int mode: Directory mode. :param bool strict: require directory to be owned by current user :raises .errors.LockError: if the directory cannot be locked :raises .errors.Error: if the directory cannot be made or verified """ try: make_or_verify_dir(directory, mode, strict) lock_dir_until_exit(directory) except OSError as error: logger.debug("Exception was:", exc_info=True) raise errors.Error(PERM_ERR_FMT.format(error)) def make_or_verify_dir(directory: str, mode: int = 0o755, strict: bool = False) -> None: """Make sure directory exists with proper permissions. :param str directory: Path to a directory. :param int mode: Directory mode. :param bool strict: require directory to be owned by current user :raises .errors.Error: if a directory already exists, but has wrong permissions or owner :raises OSError: if invalid or inaccessible file names and paths, or other arguments that have the correct type, but are not accepted by the operating system. """ try: filesystem.makedirs(directory, mode) except OSError as exception: if exception.errno == errno.EEXIST: if strict and not filesystem.check_permissions(directory, mode): raise errors.Error( "%s exists, but it should be owned by current user with" " permissions %s" % (directory, oct(mode))) else: raise def safe_open(path: str, mode: str = "w", chmod: Optional[int] = None) -> IO: """Safely open a file. :param str path: Path to a file. :param str mode: Same os `mode` for `open`. :param int chmod: Same as `mode` for `filesystem.open`, uses Python defaults if ``None``. """ open_args: Union[Tuple[()], Tuple[int]] = () if chmod is not None: open_args = (chmod,) fdopen_args: Union[Tuple[()], Tuple[int]] = () fd = filesystem.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, *open_args) return os.fdopen(fd, mode, *fdopen_args) def _unique_file(path: str, filename_pat: Callable[[int], str], count: int, chmod: int, mode: str) -> Tuple[IO, str]: while True: current_path = os.path.join(path, filename_pat(count)) try: return safe_open(current_path, chmod=chmod, mode=mode), os.path.abspath(current_path) except OSError as err: # "File exists," is okay, try a different name. if err.errno != errno.EEXIST: raise count += 1 def unique_file(path: str, chmod: int = 0o777, mode: str = "w") -> Tuple[IO, str]: """Safely finds a unique file. :param str path: path/filename.ext :param int chmod: File mode :param str mode: Open mode :returns: tuple of file object and file name """ path, tail = os.path.split(path) return _unique_file( path, filename_pat=(lambda count: "%04d_%s" % (count, tail)), count=0, chmod=chmod, mode=mode) def unique_lineage_name(path: str, filename: str, chmod: int = 0o644, mode: str = "w") -> Tuple[IO, str]: """Safely finds a unique file using lineage convention. :param str path: directory path :param str filename: proposed filename :param int chmod: file mode :param str mode: open mode :returns: tuple of file object and file name (which may be modified from the requested one by appending digits to ensure uniqueness) :raises OSError: if writing files fails for an unanticipated reason, such as a full disk or a lack of permission to write to specified location. """ preferred_path = os.path.join(path, "%s.conf" % (filename)) try: return safe_open(preferred_path, chmod=chmod), preferred_path except OSError as err: if err.errno != errno.EEXIST: raise return _unique_file( path, filename_pat=(lambda count: "%s-%04d.conf" % (filename, count)), count=1, chmod=chmod, mode=mode) def safely_remove(path: str) -> None: """Remove a file that may not exist.""" try: os.remove(path) except OSError as err: if err.errno != errno.ENOENT: raise def get_filtered_names(all_names: Set[str]) -> Set[str]: """Removes names that aren't considered valid by Let's Encrypt. :param set all_names: all names found in the configuration :returns: all found names that are considered valid by LE :rtype: set """ filtered_names = set() for name in all_names: try: filtered_names.add(enforce_le_validity(name)) except errors.ConfigurationError: logger.debug('Not suggesting name "%s"', name, exc_info=True) return filtered_names def get_os_info() -> Tuple[str, str]: """ Get OS name and version :returns: (os_name, os_version) :rtype: `tuple` of `str` """ return get_python_os_info(pretty=False) def get_os_info_ua() -> str: """ Get OS name and version string for User Agent :returns: os_ua :rtype: `str` """ if _USE_DISTRO: os_info = distro.name(pretty=True) if not _USE_DISTRO or not os_info: return " ".join(get_python_os_info(pretty=True)) return os_info def get_systemd_os_like() -> List[str]: """ Get a list of strings that indicate the distribution likeness to other distributions. :returns: List of distribution acronyms :rtype: `list` of `str` """ if _USE_DISTRO: return distro.like().split(" ") return [] def get_var_from_file(varname: str, filepath: str = "/etc/os-release") -> str: """ Get single value from a file formatted like systemd /etc/os-release :param str varname: Name of variable to fetch :param str filepath: File path of os-release file :returns: requested value :rtype: `str` """ var_string = varname+"=" if not os.path.isfile(filepath): return "" with open(filepath, 'r') as fh: contents = fh.readlines() for line in contents: if line.strip().startswith(var_string): # Return the value of var, normalized return _normalize_string(line.strip()[len(var_string):]) return "" def _normalize_string(orig: str) -> str: """ Helper function for get_var_from_file() to remove quotes and whitespaces """ return orig.replace('"', '').replace("'", "").strip() def get_python_os_info(pretty: bool = False) -> Tuple[str, str]: """ Get Operating System type/distribution and major version using python platform module :param bool pretty: If the returned OS name should be in longer (pretty) form :returns: (os_name, os_version) :rtype: `tuple` of `str` """ info = platform.system_alias( platform.system(), platform.release(), platform.version() ) os_type, os_ver, _ = info os_type = os_type.lower() if os_type.startswith('linux') and _USE_DISTRO: distro_name, distro_version = distro.name() if pretty else distro.id(), distro.version() # On arch, these values are reportedly empty strings so handle it # defensively # so handle it defensively if distro_name: os_type = distro_name if distro_version: os_ver = distro_version elif os_type.startswith('darwin'): try: proc = subprocess.run( ["/usr/bin/sw_vers", "-productVersion"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, universal_newlines=True, env=env_no_snap_for_external_calls(), ) except OSError: proc = subprocess.run( ["sw_vers", "-productVersion"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, universal_newlines=True, env=env_no_snap_for_external_calls(), ) os_ver = proc.stdout.rstrip('\n') elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] os_ver = os_ver.partition(".")[0] elif platform.win32_ver()[1]: os_ver = platform.win32_ver()[1] else: # Cases known to fall here: Cygwin python os_ver = '' return os_type, os_ver # Just make sure we don't get pwned... Make sure that it also doesn't # start with a period or have two consecutive periods <- this needs to # be done in addition to the regex EMAIL_REGEX = re.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$") def safe_email(email: str) -> bool: """Scrub email address before using it.""" if EMAIL_REGEX.match(email) is not None: return not email.startswith(".") and ".." not in email logger.error("Invalid email address: %s.", email) return False class DeprecatedArgumentAction(argparse.Action): """Action to log a warning when an argument is used.""" def __call__(self, unused1: Any, unused2: Any, unused3: Any, option_string: Optional[str] = None) -> None: logger.warning("Use of %s is deprecated.", option_string) def add_deprecated_argument(add_argument: Callable[..., None], argument_name: str, nargs: Union[str, int]) -> None: """Adds a deprecated argument with the name argument_name. Deprecated arguments are not shown in the help. If they are used on the command line, a warning is shown stating that the argument is deprecated and no other action is taken. :param callable add_argument: Function that adds arguments to an argument parser/group. :param str argument_name: Name of deprecated argument. :param nargs: Value for nargs when adding the argument to argparse. """ if DeprecatedArgumentAction not in configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE: # In version 0.12.0 ACTION_TYPES_THAT_DONT_NEED_A_VALUE was # changed from a set to a tuple. if isinstance(configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE, set): configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add( DeprecatedArgumentAction) else: configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE += ( DeprecatedArgumentAction,) add_argument(argument_name, action=DeprecatedArgumentAction, help=argparse.SUPPRESS, nargs=nargs) def enforce_le_validity(domain: str) -> str: """Checks that Let's Encrypt will consider domain to be valid. :param str domain: FQDN to check :type domain: `str` :returns: The domain cast to `str`, with ASCII-only contents :rtype: str :raises ConfigurationError: for invalid domains and cases where Let's Encrypt currently will not issue certificates """ domain = enforce_domain_sanity(domain) if not re.match("^[A-Za-z0-9.-]*$", domain): raise errors.ConfigurationError( "{0} contains an invalid character. " "Valid characters are A-Z, a-z, 0-9, ., and -.".format(domain)) labels = domain.split(".") if len(labels) < 2: raise errors.ConfigurationError( "{0} needs at least two labels".format(domain)) for label in labels: if label.startswith("-"): raise errors.ConfigurationError( 'label "{0}" in domain "{1}" cannot start with "-"'.format( label, domain)) if label.endswith("-"): raise errors.ConfigurationError( 'label "{0}" in domain "{1}" cannot end with "-"'.format( label, domain)) return domain def enforce_domain_sanity(domain: Union[str, bytes]) -> str: """Method which validates domain value and errors out if the requirements are not met. :param domain: Domain to check :type domain: `str` or `bytes` :raises ConfigurationError: for invalid domains and cases where Let's Encrypt currently will not issue certificates :returns: The domain cast to `str`, with ASCII-only contents :rtype: str """ # Unicode try: if isinstance(domain, bytes): domain = domain.decode('utf-8') domain.encode('ascii') except UnicodeError: raise errors.ConfigurationError("Non-ASCII domain names not supported. " "To issue for an Internationalized Domain Name, use Punycode.") domain = domain.lower() # Remove trailing dot domain = domain[:-1] if domain.endswith('.') else domain # Separately check for odd "domains" like "http://example.com" to fail # fast and provide a clear error message for scheme in ["http", "https"]: # Other schemes seem unlikely if domain.startswith("{0}://".format(scheme)): raise errors.ConfigurationError( "Requested name {0} appears to be a URL, not a FQDN. " "Try again without the leading \"{1}://\".".format( domain, scheme ) ) if is_ipaddress(domain): raise errors.ConfigurationError( "Requested name {0} is an IP address. The Let's Encrypt " "certificate authority will not issue certificates for a " "bare IP address.".format(domain)) # FQDN checks according to RFC 2181: domain name should be less than 255 # octets (inclusive). And each label is 1 - 63 octets (inclusive). # https://tools.ietf.org/html/rfc2181#section-11 msg = "Requested domain {0} is not a FQDN because".format(domain) if len(domain) > 255: raise errors.ConfigurationError("{0} it is too long.".format(msg)) labels = domain.split('.') for l in labels: if not l: raise errors.ConfigurationError("{0} it contains an empty label.".format(msg)) if len(l) > 63: raise errors.ConfigurationError("{0} label {1} is too long.".format(msg, l)) return domain def is_ipaddress(address: str) -> bool: """Is given address string form of IP(v4 or v6) address? :param address: address to check :type address: `str` :returns: True if address is valid IP address, otherwise return False. :rtype: bool """ try: socket.inet_pton(socket.AF_INET, address) # If this line runs it was ip address (ipv4) return True except socket.error: # It wasn't an IPv4 address, so try ipv6 try: socket.inet_pton(socket.AF_INET6, address) return True except socket.error: return False def is_wildcard_domain(domain: Union[str, bytes]) -> bool: """"Is domain a wildcard domain? :param domain: domain to check :type domain: `bytes` or `str` :returns: True if domain is a wildcard, otherwise, False :rtype: bool """ if isinstance(domain, str): return domain.startswith("*.") return domain.startswith(b"*.") def is_staging(srv: str) -> bool: """ Determine whether a given ACME server is a known test / staging server. :param str srv: the URI for the ACME server :returns: True iff srv is a known test / staging server :rtype bool: """ return srv == constants.STAGING_URI or "staging" in srv def atexit_register(func: Callable, *args: Any, **kwargs: Any) -> None: """Sets func to be called before the program exits. Special care is taken to ensure func is only called when the process that first imports this module exits rather than any child processes. :param function func: function to be called in case of an error """ atexit.register(_atexit_call, func, *args, **kwargs) def parse_loose_version(version_string: str) -> List[Union[int, str]]: """Parses a version string into its components. This code and the returned tuple is based on the now deprecated distutils.version.LooseVersion class from the Python standard library. Two LooseVersion classes and two lists as returned by this function should compare in the same way. See https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/version.py#L205-L347. :param str version_string: version string :returns: list of parsed version string components :rtype: list """ loose_version = LooseVersion(version_string) return loose_version.version_components def _atexit_call(func: Callable, *args: Any, **kwargs: Any) -> None: if _INITIAL_PID == os.getpid(): func(*args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3430836 certbot-2.9.0/certbot.egg-info/0000775000175100017510000000000014561227516015325 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421518.0 certbot-2.9.0/certbot.egg-info/PKG-INFO0000644000175100017510000001770214561227516016427 0ustar00ericaericaMetadata-Version: 2.1 Name: certbot Version: 2.9.0 Summary: ACME client Home-page: https://github.com/certbot/certbot Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities Requires-Python: >=3.8 License-File: LICENSE.txt Requires-Dist: acme>=2.9.0 Requires-Dist: ConfigArgParse>=1.5.3 Requires-Dist: configobj>=5.0.6 Requires-Dist: cryptography>=3.2.1 Requires-Dist: distro>=1.0.1 Requires-Dist: importlib_resources>=1.3.1; python_version < "3.9" Requires-Dist: importlib_metadata>=4.6; python_version < "3.10" Requires-Dist: josepy>=1.13.0 Requires-Dist: parsedatetime>=2.4 Requires-Dist: pyrfc3339 Requires-Dist: pytz>=2019.3 Requires-Dist: pywin32>=300; sys_platform == "win32" Requires-Dist: setuptools>=41.6.0 Provides-Extra: all Requires-Dist: azure-devops; extra == "all" Requires-Dist: ipdb; extra == "all" Requires-Dist: poetry>=1.2.0; extra == "all" Requires-Dist: poetry-plugin-export>=1.1.0; extra == "all" Requires-Dist: twine; extra == "all" Requires-Dist: Sphinx>=1.2; extra == "all" Requires-Dist: sphinx_rtd_theme; extra == "all" Requires-Dist: coverage; extra == "all" Requires-Dist: mypy; extra == "all" Requires-Dist: pip; extra == "all" Requires-Dist: pylint; extra == "all" Requires-Dist: pytest; extra == "all" Requires-Dist: pytest-cov; extra == "all" Requires-Dist: pytest-xdist; extra == "all" Requires-Dist: setuptools; extra == "all" Requires-Dist: tox; extra == "all" Requires-Dist: types-httplib2; extra == "all" Requires-Dist: types-pyOpenSSL; extra == "all" Requires-Dist: types-pyRFC3339; extra == "all" Requires-Dist: types-pytz; extra == "all" Requires-Dist: types-pywin32; extra == "all" Requires-Dist: types-requests; extra == "all" Requires-Dist: types-setuptools; extra == "all" Requires-Dist: types-six; extra == "all" Requires-Dist: wheel; extra == "all" Provides-Extra: dev Requires-Dist: azure-devops; extra == "dev" Requires-Dist: ipdb; extra == "dev" Requires-Dist: poetry>=1.2.0; extra == "dev" Requires-Dist: poetry-plugin-export>=1.1.0; extra == "dev" Requires-Dist: twine; extra == "dev" Provides-Extra: docs Requires-Dist: Sphinx>=1.2; extra == "docs" Requires-Dist: sphinx_rtd_theme; extra == "docs" Provides-Extra: test Requires-Dist: coverage; extra == "test" Requires-Dist: mypy; extra == "test" Requires-Dist: pip; extra == "test" Requires-Dist: pylint; extra == "test" Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: pytest-xdist; extra == "test" Requires-Dist: setuptools; extra == "test" Requires-Dist: tox; extra == "test" Requires-Dist: types-httplib2; extra == "test" Requires-Dist: types-pyOpenSSL; extra == "test" Requires-Dist: types-pyRFC3339; extra == "test" Requires-Dist: types-pytz; extra == "test" Requires-Dist: types-pywin32; extra == "test" Requires-Dist: types-requests; extra == "test" Requires-Dist: types-setuptools; extra == "test" Requires-Dist: types-six; extra == "test" Requires-Dist: wheel; extra == "test" .. This file contains a series of comments that are used to include sections of this README in other files. Do not modify these comments unless you know what you are doing. tag:intro-begin |build-status| .. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/master :target: https://dev.azure.com/certbot/certbot/_build?definitionId=5 :alt: Azure Pipelines CI status .. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/eff-certbot-lockup.png :width: 200 :alt: EFF Certbot Logo Certbot is part of EFF’s effort to encrypt the entire Internet. Secure communication over the Web relies on HTTPS, which requires the use of a digital certificate that lets browsers verify the identity of web servers (e.g., is that really google.com?). Web servers obtain their certificates from trusted third parties called certificate authorities (CAs). Certbot is an easy-to-use client that fetches a certificate from Let’s Encrypt—an open certificate authority launched by the EFF, Mozilla, and others—and deploys it to a web server. Anyone who has gone through the trouble of setting up a secure website knows what a hassle getting and maintaining a certificate is. Certbot and Let’s Encrypt can automate away the pain and let you turn on and manage HTTPS with simple commands. Using Certbot and Let's Encrypt is free. .. _installation: Getting Started --------------- The best way to get started is to use our `interactive guide `_. It generates instructions based on your configuration settings. In most cases, you’ll need `root or administrator access `_ to your web server to run Certbot. Certbot is meant to be run directly on your web server on the command line, not on your personal computer. If you’re using a hosted service and don’t have direct access to your web server, you might not be able to use Certbot. Check with your hosting provider for documentation about uploading certificates or using certificates issued by Let’s Encrypt. Contributing ------------ If you'd like to contribute to this project please read `Developer Guide `_. This project is governed by `EFF's Public Projects Code of Conduct `_. Links ===== .. Do not modify this comment unless you know what you're doing. tag:links-begin Documentation: https://certbot.eff.org/docs Software project: https://github.com/certbot/certbot Changelog: https://github.com/certbot/certbot/blob/master/certbot/CHANGELOG.md For Contributors: https://certbot.eff.org/docs/contributing.html For Users: https://certbot.eff.org/docs/using.html Main Website: https://certbot.eff.org Let's Encrypt Website: https://letsencrypt.org Community: https://community.letsencrypt.org ACME spec: `RFC 8555 `_ ACME working area in github (archived): https://github.com/ietf-wg-acme/acme .. Do not modify this comment unless you know what you're doing. tag:links-end .. Do not modify this comment unless you know what you're doing. tag:intro-end .. Do not modify this comment unless you know what you're doing. tag:features-begin Current Features ===================== * Supports multiple web servers: - Apache 2.4+ - nginx/0.8.48+ - webroot (adds files to webroot directories in order to prove control of domains and obtain certificates) - standalone (runs its own simple webserver to prove you control a domain) - other server software via `third party plugins `_ * The private key is generated locally on your system. * Can talk to the Let's Encrypt CA or optionally to other ACME compliant services. * Can get domain-validated (DV) certificates. * Can revoke certificates. * Supports ECDSA (default) and RSA certificate private keys. * Can optionally install a http -> https redirect, so your site effectively runs https only. * Fully automated. * Configuration changes are logged and can be reverted. .. Do not modify this comment unless you know what you're doing. tag:features-end ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421518.0 certbot-2.9.0/certbot.egg-info/SOURCES.txt0000664000175100017510000001674414561227516017225 0ustar00ericaericaCHANGELOG.md LICENSE.txt MANIFEST.in README.rst setup.py certbot/__init__.py certbot/achallenges.py certbot/configuration.py certbot/crypto_util.py certbot/errors.py certbot/interfaces.py certbot/main.py certbot/ocsp.py certbot/py.typed certbot/reverter.py certbot/ssl-dhparams.pem certbot/util.py certbot.egg-info/PKG-INFO certbot.egg-info/SOURCES.txt certbot.egg-info/dependency_links.txt certbot.egg-info/entry_points.txt certbot.egg-info/requires.txt certbot.egg-info/top_level.txt certbot/_internal/__init__.py certbot/_internal/account.py certbot/_internal/auth_handler.py certbot/_internal/cert_manager.py certbot/_internal/client.py certbot/_internal/constants.py certbot/_internal/eff.py certbot/_internal/error_handler.py certbot/_internal/hooks.py certbot/_internal/lock.py certbot/_internal/log.py certbot/_internal/main.py certbot/_internal/renewal.py certbot/_internal/snap_config.py certbot/_internal/storage.py certbot/_internal/updater.py certbot/_internal/cli/__init__.py certbot/_internal/cli/cli_constants.py certbot/_internal/cli/cli_utils.py certbot/_internal/cli/group_adder.py certbot/_internal/cli/helpful.py certbot/_internal/cli/paths_parser.py certbot/_internal/cli/plugins_parsing.py certbot/_internal/cli/subparsers.py certbot/_internal/cli/verb_help.py certbot/_internal/display/__init__.py certbot/_internal/display/completer.py certbot/_internal/display/dummy_readline.py certbot/_internal/display/obj.py certbot/_internal/display/util.py certbot/_internal/plugins/__init__.py certbot/_internal/plugins/disco.py certbot/_internal/plugins/manual.py certbot/_internal/plugins/null.py certbot/_internal/plugins/selection.py certbot/_internal/plugins/standalone.py certbot/_internal/plugins/webroot.py certbot/_internal/tests/__init__.py certbot/_internal/tests/account_test.py certbot/_internal/tests/auth_handler_test.py certbot/_internal/tests/cert_manager_test.py certbot/_internal/tests/cli_test.py certbot/_internal/tests/client_test.py certbot/_internal/tests/configuration_test.py certbot/_internal/tests/crypto_util_test.py certbot/_internal/tests/eff_test.py certbot/_internal/tests/error_handler_test.py certbot/_internal/tests/errors_test.py certbot/_internal/tests/helpful_test.py certbot/_internal/tests/hook_test.py certbot/_internal/tests/lock_test.py certbot/_internal/tests/log_test.py certbot/_internal/tests/main_test.py certbot/_internal/tests/ocsp_test.py certbot/_internal/tests/renewal_test.py certbot/_internal/tests/renewupdater_test.py certbot/_internal/tests/reverter_test.py certbot/_internal/tests/storage_test.py certbot/_internal/tests/util_test.py certbot/_internal/tests/compat/__init__.py certbot/_internal/tests/compat/filesystem_test.py certbot/_internal/tests/compat/misc_test.py certbot/_internal/tests/compat/os_test.py certbot/_internal/tests/display/__init__.py certbot/_internal/tests/display/completer_test.py certbot/_internal/tests/display/internal_util_test.py certbot/_internal/tests/display/obj_test.py certbot/_internal/tests/display/ops_test.py certbot/_internal/tests/display/util_test.py certbot/_internal/tests/plugins/__init__.py certbot/_internal/tests/plugins/common_test.py certbot/_internal/tests/plugins/disco_test.py certbot/_internal/tests/plugins/dns_common_test.py certbot/_internal/tests/plugins/enhancements_test.py certbot/_internal/tests/plugins/manual_test.py certbot/_internal/tests/plugins/null_test.py certbot/_internal/tests/plugins/selection_test.py certbot/_internal/tests/plugins/standalone_test.py certbot/_internal/tests/plugins/storage_test.py certbot/_internal/tests/plugins/util_test.py certbot/_internal/tests/plugins/webroot_test.py certbot/compat/__init__.py certbot/compat/_path.py certbot/compat/filesystem.py certbot/compat/misc.py certbot/compat/os.py certbot/display/__init__.py certbot/display/ops.py certbot/display/util.py certbot/plugins/__init__.py certbot/plugins/common.py certbot/plugins/dns_common.py certbot/plugins/dns_common_lexicon.py certbot/plugins/dns_test_common.py certbot/plugins/dns_test_common_lexicon.py certbot/plugins/enhancements.py certbot/plugins/storage.py certbot/plugins/util.py certbot/tests/__init__.py certbot/tests/acme_util.py certbot/tests/util.py certbot/tests/testdata/README certbot/tests/testdata/cert-5sans_512.pem certbot/tests/testdata/cert-nosans_nistp256.pem certbot/tests/testdata/cert-san_512.pem certbot/tests/testdata/cert_2048.pem certbot/tests/testdata/cert_512.pem certbot/tests/testdata/cert_512_bad.pem certbot/tests/testdata/cert_fullchain_2048.pem certbot/tests/testdata/cert_intermediate_1.pem certbot/tests/testdata/cert_intermediate_2.pem certbot/tests/testdata/cert_leaf.pem certbot/tests/testdata/cli.ini certbot/tests/testdata/csr-6sans_512.conf certbot/tests/testdata/csr-6sans_512.pem certbot/tests/testdata/csr-nonames_512.pem certbot/tests/testdata/csr-nosans_512.conf certbot/tests/testdata/csr-nosans_512.pem certbot/tests/testdata/csr-nosans_nistp256.pem certbot/tests/testdata/csr-san_512.pem certbot/tests/testdata/csr_512.der certbot/tests/testdata/csr_512.pem certbot/tests/testdata/ec_prime256v1_key.pem certbot/tests/testdata/ec_secp384r1_key.pem certbot/tests/testdata/ec_secp521r1_key.pem certbot/tests/testdata/nistp256_key.pem certbot/tests/testdata/ocsp_certificate.pem certbot/tests/testdata/ocsp_issuer_certificate.pem certbot/tests/testdata/ocsp_responder_certificate.pem certbot/tests/testdata/os-release certbot/tests/testdata/rsa2048_key.pem certbot/tests/testdata/rsa256_key.pem certbot/tests/testdata/rsa512_key.pem certbot/tests/testdata/sample-renewal-ancient.conf certbot/tests/testdata/sample-renewal-deprecated-option.conf certbot/tests/testdata/sample-renewal-ec.conf certbot/tests/testdata/sample-renewal.conf certbot/tests/testdata/webrootconftest.ini certbot/tests/testdata/sample-archive/cert1.pem certbot/tests/testdata/sample-archive/chain1.pem certbot/tests/testdata/sample-archive/fullchain1.pem certbot/tests/testdata/sample-archive/privkey1.pem certbot/tests/testdata/sample-archive-ec/cert1.pem certbot/tests/testdata/sample-archive-ec/chain1.pem certbot/tests/testdata/sample-archive-ec/fullchain1.pem certbot/tests/testdata/sample-archive-ec/privkey1.pem docs/.gitignore docs/Makefile docs/api.rst docs/challenges.rst docs/ciphers.rst docs/cli-help.txt docs/compatibility.rst docs/conf.py docs/contributing.rst docs/index.rst docs/install.rst docs/intro.rst docs/make.bat docs/packaging.rst docs/resources.rst docs/using.rst docs/what.rst docs/_static/.gitignore docs/_templates/footer.html docs/api/certbot.achallenges.rst docs/api/certbot.compat.filesystem.rst docs/api/certbot.compat.misc.rst docs/api/certbot.compat.os.rst docs/api/certbot.compat.rst docs/api/certbot.crypto_util.rst docs/api/certbot.display.ops.rst docs/api/certbot.display.rst docs/api/certbot.display.util.rst docs/api/certbot.errors.rst docs/api/certbot.interfaces.rst docs/api/certbot.main.rst docs/api/certbot.ocsp.rst docs/api/certbot.plugins.common.rst docs/api/certbot.plugins.dns_common.rst docs/api/certbot.plugins.dns_common_lexicon.rst docs/api/certbot.plugins.dns_test_common.rst docs/api/certbot.plugins.dns_test_common_lexicon.rst docs/api/certbot.plugins.enhancements.rst docs/api/certbot.plugins.rst docs/api/certbot.plugins.storage.rst docs/api/certbot.plugins.util.rst docs/api/certbot.reverter.rst docs/api/certbot.rst docs/api/certbot.tests.acme_util.rst docs/api/certbot.tests.rst docs/api/certbot.tests.util.rst docs/api/certbot.util.rst docs/man/certbot.rst examples/.gitignore examples/cli.ini examples/dev-cli.ini examples/generate-csr.sh examples/openssl.cnf examples/plugins/certbot_example_plugins.py examples/plugins/setup.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421518.0 certbot-2.9.0/certbot.egg-info/dependency_links.txt0000664000175100017510000000000114561227516021373 0ustar00ericaerica ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421518.0 certbot-2.9.0/certbot.egg-info/entry_points.txt0000664000175100017510000000044314561227516020624 0ustar00ericaerica[certbot.plugins] manual = certbot._internal.plugins.manual:Authenticator null = certbot._internal.plugins.null:Installer standalone = certbot._internal.plugins.standalone:Authenticator webroot = certbot._internal.plugins.webroot:Authenticator [console_scripts] certbot = certbot.main:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421518.0 certbot-2.9.0/certbot.egg-info/requires.txt0000664000175100017510000000161614561227516017731 0ustar00ericaericaacme>=2.9.0 ConfigArgParse>=1.5.3 configobj>=5.0.6 cryptography>=3.2.1 distro>=1.0.1 josepy>=1.13.0 parsedatetime>=2.4 pyrfc3339 pytz>=2019.3 setuptools>=41.6.0 [:python_version < "3.10"] importlib_metadata>=4.6 [:python_version < "3.9"] importlib_resources>=1.3.1 [:sys_platform == "win32"] pywin32>=300 [all] azure-devops ipdb poetry>=1.2.0 poetry-plugin-export>=1.1.0 twine Sphinx>=1.2 sphinx_rtd_theme coverage mypy pip pylint pytest pytest-cov pytest-xdist setuptools tox types-httplib2 types-pyOpenSSL types-pyRFC3339 types-pytz types-pywin32 types-requests types-setuptools types-six wheel [dev] azure-devops ipdb poetry>=1.2.0 poetry-plugin-export>=1.1.0 twine [docs] Sphinx>=1.2 sphinx_rtd_theme [test] coverage mypy pip pylint pytest pytest-cov pytest-xdist setuptools tox types-httplib2 types-pyOpenSSL types-pyRFC3339 types-pytz types-pywin32 types-requests types-setuptools types-six wheel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421518.0 certbot-2.9.0/certbot.egg-info/top_level.txt0000664000175100017510000000001014561227516020046 0ustar00ericaericacertbot ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3390837 certbot-2.9.0/docs/0000775000175100017510000000000014561227516013121 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/.gitignore0000664000175100017510000000001114561227515015100 0ustar00ericaerica/_build/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/Makefile0000664000175100017510000001564114561227515014567 0ustar00ericaerica# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/LetsEncrypt.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/LetsEncrypt.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/LetsEncrypt" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/LetsEncrypt" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3390837 certbot-2.9.0/docs/_static/0000775000175100017510000000000014561227516014547 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/_static/.gitignore0000664000175100017510000000000014561227515016524 0ustar00ericaerica././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3430836 certbot-2.9.0/docs/_templates/0000775000175100017510000000000014561227516015256 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/_templates/footer.html0000664000175100017510000000372414561227515017447 0ustar00ericaerica
{% if (theme_prev_next_buttons_location == 'bottom' or theme_prev_next_buttons_location == 'both') and (next or prev) %} {% endif %}

© Copyright 2014-2018 - The Certbot software and documentation are licensed under the Apache 2.0 license as described at https://eff.org/cb-license.

Let's Encrypt Status {%- if build_id and build_url %} {% trans build_url=build_url, build_id=build_id %} Build {{ build_id }}. {% endtrans %} {%- elif commit %} {% trans commit=commit %} Revision {{ commit }}. {% endtrans %} {%- elif last_updated %} {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %} {%- endif %}

{%- if show_sphinx %} {% trans %}Built with Sphinx using a theme provided by Read the Docs{% endtrans %}. {%- endif %} {%- block extrafooter %} {% endblock %}
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3430836 certbot-2.9.0/docs/api/0000775000175100017510000000000014561227516013672 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.achallenges.rst0000664000175100017510000000022414561227515020330 0ustar00ericaericacertbot.achallenges module ========================== .. automodule:: certbot.achallenges :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.compat.filesystem.rst0000664000175100017510000000024614561227515021534 0ustar00ericaericacertbot.compat.filesystem module ================================ .. automodule:: certbot.compat.filesystem :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.compat.misc.rst0000664000175100017510000000022414561227515020277 0ustar00ericaericacertbot.compat.misc module ========================== .. automodule:: certbot.compat.misc :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.compat.os.rst0000664000175100017510000000026414561227515017771 0ustar00ericaericacertbot.compat.os module ======================== .. automodule:: certbot.compat.os :members: chmod, umask, chown, open, mkdir, makedirs, rename, replace, access, stat, fstat ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.compat.rst0000664000175100017510000000036714561227515017355 0ustar00ericaericacertbot.compat package ====================== .. automodule:: certbot.compat :members: :undoc-members: :show-inheritance: Submodules ---------- .. toctree:: certbot.compat.filesystem certbot.compat.misc certbot.compat.os ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.crypto_util.rst0000664000175100017510000000022614561227515020441 0ustar00ericaericacertbot.crypto\_util module =========================== .. automodule:: certbot.crypto_util :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.display.ops.rst0000664000175100017510000000022414561227515020327 0ustar00ericaericacertbot.display.ops module ========================== .. automodule:: certbot.display.ops :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.display.rst0000664000175100017510000000034014561227515017526 0ustar00ericaericacertbot.display package ======================= .. automodule:: certbot.display :members: :undoc-members: :show-inheritance: Submodules ---------- .. toctree:: certbot.display.ops certbot.display.util ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.display.util.rst0000664000175100017510000000022714561227515020506 0ustar00ericaericacertbot.display.util module =========================== .. automodule:: certbot.display.util :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.errors.rst0000664000175100017510000000020514561227515017375 0ustar00ericaericacertbot.errors module ===================== .. automodule:: certbot.errors :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.interfaces.rst0000664000175100017510000000022114561227515020202 0ustar00ericaericacertbot.interfaces module ========================= .. automodule:: certbot.interfaces :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.main.rst0000664000175100017510000000017714561227515017015 0ustar00ericaericacertbot.main module =================== .. automodule:: certbot.main :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.ocsp.rst0000664000175100017510000000020314561227515017023 0ustar00ericaericacertbot.ocsp package ====================== .. automodule:: certbot.ocsp :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.plugins.common.rst0000664000175100017510000000023514561227515021034 0ustar00ericaericacertbot.plugins.common module ============================= .. automodule:: certbot.plugins.common :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.plugins.dns_common.rst0000664000175100017510000000025314561227515021700 0ustar00ericaericacertbot.plugins.dns\_common module ================================== .. automodule:: certbot.plugins.dns_common :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.plugins.dns_common_lexicon.rst0000664000175100017510000000030514561227515023417 0ustar00ericaericacertbot.plugins.dns\_common\_lexicon module =========================================== .. automodule:: certbot.plugins.dns_common_lexicon :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.plugins.dns_test_common.rst0000664000175100017510000000027414561227515022742 0ustar00ericaericacertbot.plugins.dns\_test\_common module ======================================== .. automodule:: certbot.plugins.dns_test_common :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.plugins.dns_test_common_lexicon.rst0000664000175100017510000000032614561227515024461 0ustar00ericaericacertbot.plugins.dns\_test\_common\_lexicon module ================================================= .. automodule:: certbot.plugins.dns_test_common_lexicon :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.plugins.enhancements.rst0000664000175100017510000000025714561227515022220 0ustar00ericaericacertbot.plugins.enhancements module =================================== .. automodule:: certbot.plugins.enhancements :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.plugins.rst0000664000175100017510000000066014561227515017547 0ustar00ericaericacertbot.plugins package ======================= .. automodule:: certbot.plugins :members: :undoc-members: :show-inheritance: Submodules ---------- .. toctree:: certbot.plugins.common certbot.plugins.dns_common certbot.plugins.dns_common_lexicon certbot.plugins.dns_test_common certbot.plugins.dns_test_common_lexicon certbot.plugins.enhancements certbot.plugins.storage certbot.plugins.util ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.plugins.storage.rst0000664000175100017510000000024014561227515021204 0ustar00ericaericacertbot.plugins.storage module ============================== .. automodule:: certbot.plugins.storage :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.plugins.util.rst0000664000175100017510000000022714561227515020522 0ustar00ericaericacertbot.plugins.util module =========================== .. automodule:: certbot.plugins.util :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.reverter.rst0000664000175100017510000000021314561227515017716 0ustar00ericaericacertbot.reverter module ======================= .. automodule:: certbot.reverter :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.rst0000664000175100017510000000065014561227515016066 0ustar00ericaericacertbot package =============== .. automodule:: certbot :members: :undoc-members: :show-inheritance: Subpackages ----------- .. toctree:: certbot.compat certbot.display certbot.plugins certbot.tests Submodules ---------- .. toctree:: certbot.achallenges certbot.crypto_util certbot.errors certbot.interfaces certbot.main certbot.ocsp certbot.reverter certbot.util ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.tests.acme_util.rst0000664000175100017510000000024214561227515021165 0ustar00ericaericacertbot.tests.acme\_util module =============================== .. automodule:: certbot.tests.acme_util :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.tests.rst0000664000175100017510000000033414561227515017226 0ustar00ericaericacertbot.tests package ===================== .. automodule:: certbot.tests :members: :undoc-members: :show-inheritance: Submodules ---------- .. toctree:: certbot.tests.acme_util certbot.tests.util ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.tests.util.rst0000664000175100017510000000022114561227515020175 0ustar00ericaericacertbot.tests.util module ========================= .. automodule:: certbot.tests.util :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api/certbot.util.rst0000664000175100017510000000017714561227515017046 0ustar00ericaericacertbot.util module =================== .. automodule:: certbot.util :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/api.rst0000664000175100017510000000014414561227515014422 0ustar00ericaerica================= API Documentation ================= .. toctree:: :maxdepth: 4 api/certbot ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/challenges.rst0000664000175100017510000001113514561227515015760 0ustar00ericaericaChallenges ========== To receive a certificate from Let's Encrypt certificate authority (CA), you must pass a *challenge* to prove you control each of the domain names that will be listed in the certificate. A challenge is one of a list of specified tasks that only someone who controls the domain should be able to accomplish, such as: * Posting a specified file in a specified location on a web site (the HTTP-01 challenge) * Posting a specified DNS record in the domain name system (the DNS-01 challenge) It’s possible to complete each type of challenge *automatically* (Certbot directly makes the necessary changes itself, or runs another program that does so), or *manually* (Certbot tells you to make a certain change, and you edit a configuration file of some kind in order to accomplish it). Certbot's design favors performing challenges automatically, and this is the normal case for most users of Certbot. Some plugins offer an *authenticator*, meaning that they can satisfy challenges: * Apache plugin: (HTTP-01) Tries to edit your Apache configuration files to temporarily serve files to satisfy challenges from the certificate authority. Use the Apache plugin when you're running Certbot on a web server with Apache listening on port 80. * Nginx plugin: (HTTP-01) Tries to edit your nginx configuration files to temporarily serve files to satisfy challenges from the certificate authority. Use the nginx plugin when you're running Certbot on a web server with nginx listening on port 80. * Webroot plugin: (HTTP-01) Tries to place a file where it can be served over HTTP on port 80 by a web server running on your system. Use the Webroot plugin when you're running Certbot on a web server with any server application listening on port 80 serving files from a folder on disk in response. * Standalone plugin: (HTTP-01) Tries to run a temporary web server listening on HTTP on port 80. Use the Standalone plugin if no existing program is listening to this port. * Manual plugin: (DNS-01 or HTTP-01) Either tells you what changes to make to your configuration or updates your DNS records using an external script (for DNS-01) or your webroot (for HTTP-01). Use the Manual plugin if you have the technical knowledge to make configuration changes yourself when asked to do so, and are prepared to repeat these steps every time the certificate needs to be renewed. Tips for Challenges ------------------- General tips: * Run Certbot on your web server, not on your laptop or another server. It’s usually the easiest way to get a certificate. * Use a tool like the DNSchecker at dnsstuff.com to check your DNS records to make sure there are no serious errors. A DNS error can prevent a certificate authority from issuing a certificate, even if it does not prevent your site from loading in a browser. * If you are using Apache or NGINX plugins, make sure the configuration of your Apache or NGINX server is correct. HTTP-01 Challenge ~~~~~~~~~~~~~~~~~ * Make sure the domain name exists and is already pointed to the public IP address of the server where you’re requesting the certificate. * Make sure port 80 is open, publicly reachable from the Internet, and not blocked by a router or firewall. * When using the Webroot plugin or the manual plugin, make sure the the webroot directory exists and that you specify it properly. If you set the webroot directory for example.com to `/var/www/example.com` then a file placed in `/var/www/example.com/.well-known/acme-challenge/testfile` should appear on your web site at `http://example.com/.well-known/acme-challenge/testfile` (A redirection to HTTPS is OK here and should not stop the challenge from working.) * In some web server configurations, all pages are dynamically generated by some kind of framework, usually using a database backend. In this case, there might not be a particular directory from which the web server can serve filesdirectly. Using the Webroot plugin in this case requires making a change to your web server configuration first. * Make sure your web server serves files properly from the directory where the challenge file is placed (e. g. `/.well-known/acme-challenge`) to the expected location on the website without adding a header or footer. * When using the Standalone plugin, make sure another program is not already listening to port 80 on the server. * When using the Webroot plugin, make sure there is a web server listening on port 80. DNS-01 Challenge ~~~~~~~~~~~~~~~~ * When using the manual plugin, make sure your DNS records are correctly updated; you must be able to make appropriate changes to your DNS zone in order to pass the challenge. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/ciphers.rst0000664000175100017510000003523114561227515015313 0ustar00ericaerica.. Sphinx complains that this file isn't included in any toctree, however, we currently link to it in the section about installing Certbot through Docker. Setting :orphan: below suppresses this warning. See https://www.sphinx-doc.org/en/master/usage/restructuredtext/field-lists.html#special-metadata-fields. :orphan: ============ Ciphersuites ============ .. contents:: Table of Contents :local: .. _ciphersuites: Introduction ============ Autoupdates ----------- Within certain limits, TLS server software can choose what kind of cryptography to use when a client connects. These choices can affect security, compatibility, and performance in complex ways. Most of these options are independent of a particular certificate. Certbot tries to provide defaults that we think are most useful to our users. As described below, Certbot will default to modifying server software's cryptographic settings to keep these up-to-date with what we think are appropriate defaults when new versions of the Certbot are installed (for example, by an operating system package manager). When this feature is implemented, this document will be updated to describe how to disable these automatic changes. Cryptographic choices --------------------- Software that uses cryptography must inevitably make choices about what kind of cryptography to use and how. These choices entail assumptions about how well particular cryptographic mechanisms resist attack, and what trade-offs are available and appropriate. The choices are constrained by compatibility issues (in order to interoperate with other software, an implementation must agree to use cryptographic mechanisms that the other side also supports) and protocol issues (cryptographic mechanisms must be specified in protocols and there must be a way to agree to use them in a particular context). The best choices for a particular application may change over time in response to new research, new standardization events, changes in computer hardware, and changes in the prevalence of legacy software. Much important research on cryptanalysis and cryptographic vulnerabilities is unpublished because many researchers have been working in the interest of improving some entities' communications security while weakening, or failing to improve, others' security. But important information that improves our understanding of the state of the art is published regularly. When enabling TLS support in a compatible web server (which is a separate step from obtaining a certificate), Certbot has the ability to update that web server's TLS configuration. Again, this is *different from the cryptographic particulars of the certificate itself*; the certificate as of the initial release will be RSA-signed using one of Let's Encrypt's 2048-bit RSA keys, and will describe the subscriber's RSA public key ("subject public key") of at least 2048 bits, which is used for key establishment. Note that the subscriber's RSA public key can be used in a wide variety of key establishment methods, most of which do not use RSA directly for key exchange, but only for authenticating the server! For example, in DHE and ECDHE key exchanges, the subject public key is just used to sign other parameters for authentication. You do not have to "use RSA" for other purposes just because you're using an RSA key for authentication. The certificate doesn't specify other cryptographic or ciphersuite particulars; for example, it doesn't say whether or not parties should use a particular symmetric algorithm like 3DES, or what cipher modes they should use. All of these details are negotiated between client and server independent of the content of the ciphersuite. The Let's Encrypt project hopes to provide useful defaults that reflect good security choices with respect to the publicly-known state of the art. However, the Let's Encrypt certificate authority does *not* dictate end-users' security policy, and any site is welcome to change its preferences in accordance with its own policy or its administrators' preferences, and use different cryptographic mechanisms or parameters, or a different priority order, than the defaults provided by Certbot. If you don't use Certbot to configure your server directly, because the client doesn't integrate with your server software or because you chose not to use this integration, then the cryptographic defaults haven't been modified, and the cryptography chosen by the server will still be whatever the default for your software was. For example, if you obtain a certificate using *standalone* mode and then manually install it in an IMAP or LDAP server, your cryptographic settings will not be modified by the client in any way. Sources of defaults ------------------- Initially, Certbot will configure users' servers to use the cryptographic defaults recommended by the Mozilla project. These settings are well-reasoned recommendations that carefully consider client software compatibility. They are described at https://wiki.mozilla.org/Security/Server_Side_TLS and the version implemented by Certbot will be the version that was most current as of the release date of each client version. Mozilla offers three separate sets of cryptographic options, which trade off security and compatibility differently. These are referred to as the "Modern", "Intermediate", and "Old" configurations (in order from most secure to least secure, and least-backwards compatible to most-backwards compatible). The client will follow the Mozilla defaults for the *Intermediate* configuration by default, at least with regards to ciphersuites and TLS versions. Mozilla's web site describes which client software will be compatible with each configuration. You can also use the Qualys SSL Labs site to test your server and see whether it will be compatible with particular software versions. The Let's Encrypt project expects to follow the Mozilla recommendations in the future as those recommendations are updated. (For example, some users have proposed prioritizing a new ciphersuite known as ``0xcc13`` which uses the ChaCha and Poly1305 algorithms, and which is already implemented by the Chrome browser. Mozilla has delayed recommending ``0xcc13`` over compatibility and standardization concerns, but is likely to recommend it in the future once these concerns have been addressed. At that point, Certbot would likely follow the Mozilla recommendations and favor the use of this ciphersuite as well.) The Let's Encrypt project may deviate from the Mozilla recommendations in the future if good cause is shown and we believe our users' priorities would be well-served by doing so. In general, please address relevant proposals for changing priorities to the Mozilla security team first, before asking the Certbot developers to change Certbot's priorities. The Mozilla security team is likely to have more resources and expertise to bring to bear on evaluating reasons why its recommendations should be updated. The Let's Encrypt project will entertain proposals to create a *very* small number of alternative configurations (apart from Modern, Intermediate, and Old) that there's reason to believe would be widely used by sysadmins; this would usually be a preferable course to modifying an existing configuration. For example, if many sysadmins want their servers configured to track a different expert recommendation, Certbot could add an option to do so. Resources for recommendations ----------------------------- In the course of considering how to handle this issue, we received recommendations with sources of expert guidance on ciphersuites and other cryptographic parameters. We're grateful to everyone who contributed suggestions. The recommendations we received are available under Feedback_. Certbot users are welcome to review these authorities to better inform their own cryptographic parameter choices. We also welcome suggestions of other resources to add to this list. Please keep in mind that different recommendations may reflect different priorities or evaluations of trade-offs, especially related to compatibility! Feedback ======== We receive lots of feedback on the type of ciphersuites that Let's Encrypt supports and list some collated feedback below. This section aims to track suggestions and references that people have offered or identified to improve the ciphersuites that Let's Encrypt enables when configuring TLS on servers. Because of the Chatham House Rule applicable to some of the discussions, people are *not* individually credited for their suggestions, but most suggestions here were made or found by other people, and I thank them for their contributions. Some people provided rationale information mostly having to do with compatibility of particular user-agents (especially UAs that don't support ECC, or that don't support DH groups > 1024 bits). Some ciphersuite configurations have been chosen to try to increase compatibility with older UAs while allowing newer UAs to negotiate stronger crypto. For example, some configurations forego forward secrecy entirely for connections from old UAs, like by offering ECDHE and RSA key exchange, but no DHE at all. (There are UAs that can fail the negotiation completely if a DHE ciphersuite with prime > 1024 bits is offered.) References ---------- RFC 7575 ~~~~~~~~ IETF has published a BCP document, RFC 7525, "Recommendations for Secure Use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS)" https://datatracker.ietf.org/doc/rfc7525/ BetterCrypto.org ~~~~~~~~~~~~~~~~ BetterCrypto.org, a collaboration of mostly European IT security experts, has published a draft paper, "Applied Crypto Hardening" https://bettercrypto.org/ RFC 7919 ~~~~~~~~ IETF has published a document, RFC 7919, "Negotiated Discrete Log Diffie-Hellman Ephemeral Parameters for TLS". It advocates using *standardized* DH groups in all cases, not individually-chosen ones (mostly because of the Triple Handshake attack which can involve maliciously choosing invalid DH groups). The RFC provides a list of recommended groups, with primes beginning at 2048 bits and going up from there. It also has a new protocol mechanism for agreeing to use these groups, with the possibility of backwards compatibility (and use of weaker DH groups) for older clients and servers that don't know about this mechanism. https://datatracker.ietf.org/doc/html/rfc7919 Mozilla ~~~~~~~ Mozilla's general server configuration guidance is available at https://wiki.mozilla.org/Security/Server_Side_TLS Mozilla has also produced a configuration generator: https://ssl-config.mozilla.org Dutch National Cyber Security Centre ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The Dutch National Cyber Security Centre has published guidance on "ICT-beveiligingsrichtlijnen voor Transport Layer Security (TLS)" ("IT Security Guidelines for Transport Layer Security (TLS)"). These are available only in Dutch at https://web.archive.org/web/20190516085116/https://www.ncsc.nl/actueel/whitepapers/ict-beveiligingsrichtlijnen-voor-transport-layer-security-tls.html I have access to an English-language summary of the recommendations. Keylength.com ~~~~~~~~~~~~~ Damien Giry collects recommendations by academic researchers and standards organizations about keylengths for particular cryptoperiods, years, or security levels. The keylength recommendations of the various sources are summarized in a chart. This site has been updated over time and includes expert guidance from eight sources published between 2000 and 2017. https://www.keylength.com/ NIST ~~~~ NIST published its "NIST Special Publication 800-52 Revision 2: Guidelines for the Selection, Configuration, and Use of Transport Layer Security (TLS) Implementations" https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-52r2.pdf and its "NIST Special Publication 800-57: Recommendation for Key Management – Part 1: General (Revision 5)" https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf ENISA ~~~~~ ENISA published its "Algorithms, Key Sizes and Parameters Report - 2013" https://www.enisa.europa.eu/activities/identity-and-trust/library/deliverables/algorithms-key-sizes-and-parameters-report WeakDH/Logjam ------------- The WeakDH/Logjam research has thrown into question the safety of some existing practice using DH ciphersuites, especially the use of standardized groups with a prime ≤ 1024 bits. The authors provided detailed guidance, including ciphersuite lists, at https://weakdh.org/sysadmin.html These lists may have been derived from Mozilla's recommendations. One of the authors clarified his view of the priorities for various changes as a result of the research at https://web.archive.org/web/20150526022820/https://www.ietf.org/mail-archive/web/tls/current/msg16496.html In particular, he supports ECDHE and also supports the use of the standardized groups in the FF-DHE Internet-Draft mentioned above (which isn't clear from the group's original recommendations). Particular sites' opinions or configurations -------------------------------------------- Amazon ELB ~~~~~~~~~~ Amazon ELB explains its current ciphersuite choices at https://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elb-security-policy-table.html U.S. Government 18F ~~~~~~~~~~~~~~~~~~~ The 18F site (https://18f.gsa.gov/) is using :: ssl_ciphers 'kEECDH+ECDSA+AES128 kEECDH+ECDSA+AES256 kEECDH+AES128 kEECDH+AES256 kEDH+AES128 kEDH+AES256 DES-CBC3-SHA +SHA !aNULL !eNULL !LOW !MD5 !EXP !DSS !PSK !SRP !kECDH !CAMELLIA !RC4 !SEED'; Duraconf ~~~~~~~~ The Duraconf project collects particular configuration files, with an apparent focus on avoiding the use of obsolete symmetric ciphers and hash functions, and favoring forward secrecy while not requiring it. https://github.com/ioerror/duraconf Site scanning or rating tools ----------------------------- Qualys SSL Labs ~~~~~~~~~~~~~~~ Qualys offers the best-known TLS security scanner, maintained by Ivan Ristić. https://www.ssllabs.com/ Dutch NCSC ~~~~~~~~~~ The Dutch NCSC, mentioned above, has also made available its own site security scanner which indicates how well sites comply with the recommendations. https://en.internet.nl/ Java compatibility issue ------------------------ A lot of backward-compatibility concerns have to do with Java hard-coding DHE primes to a 1024-bit limit, accepting DHE ciphersuites in negotiation, and then aborting the connection entirely if a prime > 1024 bits is presented. The simple summary is that servers offering a Java-compatible DHE ciphersuite in preference to other Java-compatible ciphersuites, and then presenting a DH group with a prime > 1024 bits, will be completely incompatible with clients running some versions of Java. (This may also be the case with very old MSIE versions...?) There are various strategies for dealing with this, and maybe we can document the options here. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/cli-help.txt0000664000175100017510000012042314561227515015360 0ustar00ericaericausage: certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ... Certbot can obtain and install HTTPS/TLS/SSL certificates. By default, it will attempt to use a webserver both for obtaining and installing the certificate. The most common SUBCOMMANDS and flags are: obtain, install, and renew certificates: (default) run Obtain & install a certificate in your current webserver certonly Obtain or renew a certificate, but do not install it renew Renew all previously obtained certificates that are near expiry enhance Add security enhancements to your existing configuration -d DOMAINS Comma-separated list of domains to obtain a certificate for --apache Use the Apache plugin for authentication & installation --standalone Run a standalone webserver for authentication --nginx Use the Nginx plugin for authentication & installation --webroot Place files in a server's webroot folder for authentication --manual Obtain certificates interactively, or using shell script hooks -n Run non-interactively --test-cert Obtain a test certificate from a staging server --dry-run Test "renew" or "certonly" without saving any certificates to disk manage certificates: certificates Display information about certificates you have from Certbot revoke Revoke a certificate (supply --cert-name or --cert-path) delete Delete a certificate (supply --cert-name) reconfigure Update a certificate's configuration (supply --cert-name) manage your account: register Create an ACME account unregister Deactivate an ACME account update_account Update an ACME account show_account Display account details --agree-tos Agree to the ACME server's Subscriber Agreement -m EMAIL Email address for important account notifications options: -h, --help show this help message and exit -c CONFIG_FILE, --config CONFIG_FILE path to config file (default: /etc/letsencrypt/cli.ini and ~/.config/letsencrypt/cli.ini) -v, --verbose This flag can be used multiple times to incrementally increase the verbosity of output, e.g. -vvv. (default: 0) --max-log-backups MAX_LOG_BACKUPS Specifies the maximum number of backup logs that should be kept by Certbot's built in log rotation. Setting this flag to 0 disables log rotation entirely, causing Certbot to always append to the same log file. (default: 1000) -n, --non-interactive, --noninteractive Run without ever asking for user input. This may require additional command line flags; the client will try to explain which ones are required if it finds one missing (default: False) --force-interactive Force Certbot to be interactive even if it detects it's not being run in a terminal. This flag cannot be used with the renew subcommand. (default: False) -d DOMAIN, --domains DOMAIN, --domain DOMAIN Domain names to include. For multiple domains you can use multiple -d flags or enter a comma separated list of domains as a parameter. All domains will be included as Subject Alternative Names on the certificate. The first domain will be used as the certificate name, unless otherwise specified or if you already have a certificate with the same name. In the case of a name conflict, a number like -0001 will be appended to the certificate name. (default: Ask) --eab-kid EAB_KID Key Identifier for External Account Binding (default: None) --eab-hmac-key EAB_HMAC_KEY HMAC key for External Account Binding (default: None) --cert-name CERTNAME Certificate name to apply. This name is used by Certbot for housekeeping and in file paths; it doesn't affect the content of the certificate itself. Certificate name cannot contain filepath separators (i.e. '/' or '\', depending on the platform). To see certificate names, run 'certbot certificates'. When creating a new certificate, specifies the new certificate's name. (default: the first provided domain or the name of an existing certificate on your system for the same domains) --dry-run Perform a test run against the Let's Encrypt staging server, obtaining test (invalid) certificates but not saving them to disk. This can only be used with the 'certonly' and 'renew' subcommands. It may trigger webserver reloads to temporarily modify & roll back configuration files. --pre-hook and --post-hook commands run by default. --deploy-hook commands do not run, unless enabled by --run-deploy-hooks. The test server may be overridden with --server. (default: False) --debug-challenges After setting up challenges, wait for user input before submitting to CA. When used in combination with the `-v` option, the challenge URLs or FQDNs and their expected return values are shown. (default: False) --preferred-chain PREFERRED_CHAIN Set the preferred certificate chain. If the CA offers multiple certificate chains, prefer the chain whose topmost certificate was issued from this Subject Common Name. If no match, the default offered chain will be used. (default: None) --preferred-challenges PREF_CHALLS A sorted, comma delimited list of the preferred challenge to use during authorization with the most preferred challenge listed first (Eg, "dns" or "http,dns"). Not all plugins support all challenges. See https://certbot.eff.org/docs/using.html#plugins for details. ACME Challenges are versioned, but if you pick "http" rather than "http-01", Certbot will select the latest version automatically. (default: []) --issuance-timeout ISSUANCE_TIMEOUT This option specifies how long (in seconds) Certbot will wait for the server to issue a certificate. (default: 90) --user-agent USER_AGENT Set a custom user agent string for the client. User agent strings allow the CA to collect high level statistics about success rates by OS, plugin and use case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to "". (default: CertbotACMEClient/2.8.0 (certbot; OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel). The flags encoded in the user agent are: --duplicate, --force-renew, --allow-subset-of-names, -n, and whether any hooks are set. --user-agent-comment USER_AGENT_COMMENT Add a comment to the default user agent string. May be used when repackaging Certbot or calling it from another tool to allow additional statistical data to be collected. Ignored if --user-agent is set. (Example: Foo-Wrapper/1.0) (default: None) automation: Flags for automating execution & other tweaks --keep-until-expiring, --keep, --reinstall If the requested certificate matches an existing certificate, always keep the existing one until it is due for renewal (for the 'run' subcommand this means reinstall the existing certificate). (default: Ask) --expand If an existing certificate is a strict subset of the requested names, always expand and replace it with the additional names. (default: Ask) --version show program's version number and exit --force-renewal, --renew-by-default If a certificate already exists for the requested domains, renew it now, regardless of whether it is near expiry. (Often --keep-until-expiring is more appropriate). Also implies --expand. (default: False) --renew-with-new-domains If a certificate already exists for the requested certificate name but does not match the requested domains, renew it now, regardless of whether it is near expiry. (default: False) --reuse-key When renewing, use the same private key as the existing certificate. (default: False) --no-reuse-key When renewing, do not use the same private key as the existing certificate. Not reusing private keys is the default behavior of Certbot. This option may be used to unset --reuse-key on an existing certificate. (default: False) --new-key When renewing or replacing a certificate, generate a new private key, even if --reuse-key is set on the existing certificate. Combining --new-key and --reuse- key will result in the private key being replaced and then reused in future renewals. (default: False) --allow-subset-of-names When performing domain validation, do not consider it a failure if authorizations can not be obtained for a strict subset of the requested domains. This may be useful for allowing renewals for multiple domains to succeed even if some domains no longer point at this system. This option cannot be used with --csr. (default: False) --agree-tos Agree to the ACME Subscriber Agreement (default: Ask) --duplicate Allow making a certificate lineage that duplicates an existing one (both can be renewed in parallel) (default: False) -q, --quiet Silence all output except errors. Useful for automation via cron. Implies --non-interactive. (default: False) security: Security parameters & server settings --rsa-key-size N Size of the RSA key. (default: 2048) --key-type {rsa,ecdsa} Type of generated private key. Only *ONE* per invocation can be provided at this time. (default: ecdsa) --elliptic-curve N The SECG elliptic curve name to use. Please see RFC 8446 for supported values. (default: secp256r1) --must-staple Adds the OCSP Must-Staple extension to the certificate. Autoconfigures OCSP Stapling for supported setups (Apache version >= 2.3.3 ). (default: False) --redirect Automatically redirect all HTTP traffic to HTTPS for the newly authenticated vhost. (default: redirect enabled for install and run, disabled for enhance) --no-redirect Do not automatically redirect all HTTP traffic to HTTPS for the newly authenticated vhost. (default: redirect enabled for install and run, disabled for enhance) --hsts Add the Strict-Transport-Security header to every HTTP response. Forcing browser to always use SSL for the domain. Defends against SSL Stripping. (default: None) --uir Add the "Content-Security-Policy: upgrade-insecure- requests" header to every HTTP response. Forcing the browser to use https:// for every http:// resource. (default: None) --staple-ocsp Enables OCSP Stapling. A valid OCSP response is stapled to the certificate that the server offers during TLS. (default: None) --strict-permissions Require that all configuration files are owned by the current user; only needed if your config is somewhere unsafe like /tmp/ (default: False) --auto-hsts Gradually increasing max-age value for HTTP Strict Transport Security security header (default: False) testing: The following flags are meant for testing and integration purposes only. --run-deploy-hooks When performing a test run using `--dry-run` or `reconfigure`, run any applicable deploy hooks. This includes hooks set on the command line, saved in the certificate's renewal configuration file, or present in the renewal-hooks directory. To exclude directory hooks, use --no-directory-hooks. The hook(s) will only be run if the dry run succeeds, and will use the current active certificate, not the temporary test certificate acquired during the dry run. This flag is recommended when modifying the deploy hook using `reconfigure`. (default: False) --test-cert, --staging Use the Let's Encrypt staging server to obtain or revoke test (invalid) certificates; equivalent to --server https://acme- staging-v02.api.letsencrypt.org/directory (default: False) --debug Show tracebacks in case of errors (default: False) --no-verify-ssl Disable verification of the ACME server's certificate. The root certificates trusted by Certbot can be overriden by setting the REQUESTS_CA_BUNDLE environment variable. (default: False) --http-01-port HTTP01_PORT Port used in the http-01 challenge. This only affects the port Certbot listens on. A conforming ACME server will still attempt to connect on port 80. (default: 80) --http-01-address HTTP01_ADDRESS The address the server listens to during http-01 challenge. (default: ) --https-port HTTPS_PORT Port used to serve HTTPS. This affects which port Nginx will listen on after a LE certificate is installed. (default: 443) --break-my-certs Be willing to replace or renew valid certificates with invalid (testing/staging) certificates (default: False) paths: Flags for changing execution paths & servers --cert-path CERT_PATH Path to where certificate is saved (with certonly --csr), installed from, or revoked (default: None) --key-path KEY_PATH Path to private key for certificate installation or revocation (if account key is missing) (default: None) --fullchain-path FULLCHAIN_PATH Accompanying path to a full certificate chain (certificate plus chain). (default: None) --chain-path CHAIN_PATH Accompanying path to a certificate chain. (default: None) --config-dir CONFIG_DIR Configuration directory. (default: /etc/letsencrypt) --work-dir WORK_DIR Working directory. (default: /var/lib/letsencrypt) --logs-dir LOGS_DIR Logs directory. (default: /var/log/letsencrypt) --server SERVER ACME Directory Resource URI. (default: https://acme-v02.api.letsencrypt.org/directory) manage: Various subcommands and flags are available for managing your certificates: certificates List certificates managed by Certbot delete Clean up all files related to a certificate renew Renew all certificates (or one specified with --cert- name) revoke Revoke a certificate specified with --cert-path or --cert-name reconfigure Update renewal configuration for a certificate specified by --cert-name run: Options for obtaining & installing certificates certonly: Options for modifying how a certificate is obtained --csr CSR Path to a Certificate Signing Request (CSR) in DER or PEM format. Currently --csr only works with the 'certonly' subcommand. (default: None) renew: The 'renew' subcommand will attempt to renew any certificates previously obtained if they are close to expiry, and print a summary of the results. By default, 'renew' will reuse the plugins and options used to obtain or most recently renew each certificate. You can test whether future renewals will succeed with `--dry-run`. Individual certificates can be renewed with the `--cert-name` option. Hooks are available to run commands before and after renewal; see https://certbot.eff.org/docs/using.html#renewal for more information on these. --pre-hook PRE_HOOK Command to be run in a shell before obtaining any certificates. Unless --disable-hook-validation is used, the command’s first word must be the absolute pathname of an executable or one found via the PATH environment variable. Intended primarily for renewal, where it can be used to temporarily shut down a webserver that might conflict with the standalone plugin. This will only be called if a certificate is actually to be obtained/renewed. When renewing several certificates that have identical pre-hooks, only the first will be executed. (default: None) --post-hook POST_HOOK Command to be run in a shell after attempting to obtain/renew certificates. Unless --disable-hook- validation is used, the command’s first word must be the absolute pathname of an executable or one found via the PATH environment variable. Can be used to deploy renewed certificates, or to restart any servers that were stopped by --pre-hook. This is only run if an attempt was made to obtain/renew a certificate. If multiple renewed certificates have identical post- hooks, only one will be run. (default: None) --deploy-hook DEPLOY_HOOK Command to be run in a shell once for each successfully issued certificate. Unless --disable- hook-validation is used, the command’s first word must be the absolute pathname of an executable or one found via the PATH environment variable. For this command, the shell variable $RENEWED_LINEAGE will point to the config live subdirectory (for example, "/etc/letsencrypt/live/example.com") containing the new certificates and keys; the shell variable $RENEWED_DOMAINS will contain a space-delimited list of renewed certificate domains (for example, "example.com www.example.com") (default: None) --disable-hook-validation Ordinarily the commands specified for --pre- hook/--post-hook/--deploy-hook will be checked for validity, to see if the programs being run are in the $PATH, so that mistakes can be caught early, even when the hooks aren't being run just yet. The validation is rather simplistic and fails if you use more advanced shell constructs, so you can use this switch to disable it. (default: False) --no-directory-hooks Disable running executables found in Certbot's hook directories during renewal. (default: False) --disable-renew-updates Disable automatic updates to your server configuration that would otherwise be done by the selected installer plugin, and triggered when the user executes "certbot renew", regardless of if the certificate is renewed. This setting does not apply to important TLS configuration updates. (default: False) --no-autorenew Disable auto renewal of certificates. (default: False) certificates: List certificates managed by Certbot delete: Options for deleting a certificate revoke: Options for revocation of certificates --reason {unspecified,keycompromise,affiliationchanged,superseded,cessationofoperation} Specify reason for revoking certificate. (default: unspecified) --delete-after-revoke Delete certificates after revoking them, along with all previous and later versions of those certificates. (default: None) --no-delete-after-revoke Do not delete certificates after revoking them. This option should be used with caution because the 'renew' subcommand will attempt to renew undeleted revoked certificates. (default: None) register: Options for account registration --register-unsafely-without-email Specifying this flag enables registering an account with no email address. This is strongly discouraged, because you will be unable to receive notice about impending expiration or revocation of your certificates or problems with your Certbot installation that will lead to failure to renew. (default: False) -m EMAIL, --email EMAIL Email used for registration and recovery contact. Use comma to register multiple emails, ex: u1@example.com,u2@example.com. (default: Ask). --eff-email Share your e-mail address with EFF (default: None) --no-eff-email Don't share your e-mail address with EFF (default: None) update_account: Options for account modification unregister: Options for account deactivation. --account ACCOUNT_ID Account ID to use (default: None) install: Options for modifying how a certificate is deployed rollback: Options for rolling back server configuration changes --checkpoints N Revert configuration N number of checkpoints. (default: 1) plugins: Options for the "plugins" subcommand --init Initialize plugins. (default: False) --prepare Initialize and prepare plugins. (default: False) --authenticators Limit to authenticator plugins only. (default: None) --installers Limit to installer plugins only. (default: None) enhance: Helps to harden the TLS configuration by adding security enhancements to already existing configuration. show_account: Options useful for the "show_account" subcommand: reconfigure: Common options that may be updated with the "reconfigure" subcommand: plugins: Plugin Selection: Certbot client supports an extensible plugins architecture. See 'certbot plugins' for a list of all installed plugins and their names. You can force a particular plugin by setting options provided below. Running --help will list flags specific to that plugin. --configurator CONFIGURATOR Name of the plugin that is both an authenticator and an installer. Should not be used together with --authenticator or --installer. (default: Ask) -a AUTHENTICATOR, --authenticator AUTHENTICATOR Authenticator plugin name. (default: None) -i INSTALLER, --installer INSTALLER Installer plugin name (also used to find domains). (default: None) --apache Obtain and install certificates using Apache (default: False) --nginx Obtain and install certificates using Nginx (default: False) --standalone Obtain certificates using a "standalone" webserver. (default: False) --manual Provide laborious manual instructions for obtaining a certificate (default: False) --webroot Obtain certificates by placing files in a webroot directory. (default: False) --dns-cloudflare Obtain certificates using a DNS TXT record (if you are using Cloudflare for DNS). (default: False) --dns-digitalocean Obtain certificates using a DNS TXT record (if you are using DigitalOcean for DNS). (default: False) --dns-dnsimple Obtain certificates using a DNS TXT record (if you are using DNSimple for DNS). (default: False) --dns-dnsmadeeasy Obtain certificates using a DNS TXT record (if you are using DNS Made Easy for DNS). (default: False) --dns-gehirn Obtain certificates using a DNS TXT record (if you are using Gehirn Infrastructure Service for DNS). (default: False) --dns-google Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS). (default: False) --dns-linode Obtain certificates using a DNS TXT record (if you are using Linode for DNS). (default: False) --dns-luadns Obtain certificates using a DNS TXT record (if you are using LuaDNS for DNS). (default: False) --dns-nsone Obtain certificates using a DNS TXT record (if you are using NS1 for DNS). (default: False) --dns-ovh Obtain certificates using a DNS TXT record (if you are using OVH for DNS). (default: False) --dns-rfc2136 Obtain certificates using a DNS TXT record (if you are using BIND for DNS). (default: False) --dns-route53 Obtain certificates using a DNS TXT record (if you are using Route53 for DNS). (default: False) --dns-sakuracloud Obtain certificates using a DNS TXT record (if you are using Sakura Cloud for DNS). (default: False) apache: Apache Web Server plugin (Please note that the default values of the Apache plugin options change depending on the operating system Certbot is run on.) --apache-enmod APACHE_ENMOD Path to the Apache 'a2enmod' binary (default: None) --apache-dismod APACHE_DISMOD Path to the Apache 'a2dismod' binary (default: None) --apache-le-vhost-ext APACHE_LE_VHOST_EXT SSL vhost configuration extension (default: -le- ssl.conf) --apache-server-root APACHE_SERVER_ROOT Apache server root directory (default: /etc/apache2) --apache-vhost-root APACHE_VHOST_ROOT Apache server VirtualHost configuration root (default: None) --apache-logs-root APACHE_LOGS_ROOT Apache server logs directory (default: /var/log/apache2) --apache-challenge-location APACHE_CHALLENGE_LOCATION Directory path for challenge configuration (default: /etc/apache2) --apache-handle-modules APACHE_HANDLE_MODULES Let installer handle enabling required modules for you (Only Ubuntu/Debian currently) (default: False) --apache-handle-sites APACHE_HANDLE_SITES Let installer handle enabling sites for you (Only Ubuntu/Debian currently) (default: False) --apache-ctl APACHE_CTL Full path to Apache control script (default: apache2ctl) --apache-bin APACHE_BIN Full path to apache2/httpd binary (default: None) dns-cloudflare: Obtain certificates using a DNS TXT record (if you are using Cloudflare for DNS). --dns-cloudflare-propagation-seconds DNS_CLOUDFLARE_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 10) --dns-cloudflare-credentials DNS_CLOUDFLARE_CREDENTIALS Cloudflare credentials INI file. (default: None) dns-digitalocean: Obtain certificates using a DNS TXT record (if you are using DigitalOcean for DNS). --dns-digitalocean-propagation-seconds DNS_DIGITALOCEAN_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 10) --dns-digitalocean-credentials DNS_DIGITALOCEAN_CREDENTIALS DigitalOcean credentials INI file. (default: None) dns-dnsimple: Obtain certificates using a DNS TXT record (if you are using DNSimple for DNS). --dns-dnsimple-propagation-seconds DNS_DNSIMPLE_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 30) --dns-dnsimple-credentials DNS_DNSIMPLE_CREDENTIALS DNSimple credentials INI file. (default: None) dns-dnsmadeeasy: Obtain certificates using a DNS TXT record (if you are using DNS Made Easy for DNS). --dns-dnsmadeeasy-propagation-seconds DNS_DNSMADEEASY_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 60) --dns-dnsmadeeasy-credentials DNS_DNSMADEEASY_CREDENTIALS DNS Made Easy credentials INI file. (default: None) dns-gehirn: Obtain certificates using a DNS TXT record (if you are using Gehirn Infrastructure Service for DNS). --dns-gehirn-propagation-seconds DNS_GEHIRN_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 30) --dns-gehirn-credentials DNS_GEHIRN_CREDENTIALS Gehirn Infrastructure Service credentials file. (default: None) dns-google: Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS for DNS). --dns-google-propagation-seconds DNS_GOOGLE_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 60) --dns-google-credentials DNS_GOOGLE_CREDENTIALS Path to Google Cloud DNS service account JSON file to use instead of relying on Application Default Credentials (ADC). (See https://cloud.google.com/docs/ authentication/application-default-credentials for information about ADC, https://developers.google.com/i dentity/protocols/OAuth2ServiceAccount#creatinganaccou nt for information about creating a service account, and https://cloud.google.com/dns/access- control#permissions_and_roles for information about the permissions required to modify Cloud DNS records.) (default: None) --dns-google-project DNS_GOOGLE_PROJECT The ID of the Google Cloud project that the Google Cloud DNS managed zone(s) reside in. This will be determined automatically if not specified. (default: None) dns-linode: Obtain certificates using a DNS TXT record (if you are using Linode for DNS). --dns-linode-propagation-seconds DNS_LINODE_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 120) --dns-linode-credentials DNS_LINODE_CREDENTIALS Linode credentials INI file. (default: None) dns-luadns: Obtain certificates using a DNS TXT record (if you are using LuaDNS for DNS). --dns-luadns-propagation-seconds DNS_LUADNS_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 30) --dns-luadns-credentials DNS_LUADNS_CREDENTIALS LuaDNS credentials INI file. (default: None) dns-nsone: Obtain certificates using a DNS TXT record (if you are using NS1 for DNS). --dns-nsone-propagation-seconds DNS_NSONE_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 30) --dns-nsone-credentials DNS_NSONE_CREDENTIALS NS1 credentials file. (default: None) dns-ovh: Obtain certificates using a DNS TXT record (if you are using OVH for DNS). --dns-ovh-propagation-seconds DNS_OVH_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 120) --dns-ovh-credentials DNS_OVH_CREDENTIALS OVH credentials INI file. (default: None) dns-rfc2136: Obtain certificates using a DNS TXT record (if you are using BIND for DNS). --dns-rfc2136-propagation-seconds DNS_RFC2136_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 60) --dns-rfc2136-credentials DNS_RFC2136_CREDENTIALS RFC 2136 credentials INI file. (default: None) dns-route53: Obtain certificates using a DNS TXT record (if you are using AWS Route53 for DNS). dns-sakuracloud: Obtain certificates using a DNS TXT record (if you are using Sakura Cloud for DNS). --dns-sakuracloud-propagation-seconds DNS_SAKURACLOUD_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (default: 90) --dns-sakuracloud-credentials DNS_SAKURACLOUD_CREDENTIALS Sakura Cloud credentials file. (default: None) manual: Authenticate through manual configuration or custom shell scripts. When using shell scripts, an authenticator script must be provided. The environment variables available to this script depend on the type of challenge. $CERTBOT_DOMAIN will always contain the domain being authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION is the validation string, and $CERTBOT_TOKEN is the filename of the resource requested when performing an HTTP-01 challenge. An additional cleanup script can also be provided and can use the additional variable $CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script. For both authenticator and cleanup script, on HTTP-01 and DNS-01 challenges, $CERTBOT_REMAINING_CHALLENGES will be equal to the number of challenges that remain after the current one, and $CERTBOT_ALL_DOMAINS contains a comma-separated list of all domains that are challenged for the current certificate. --manual-auth-hook MANUAL_AUTH_HOOK Path or command to execute for the authentication script (default: None) --manual-cleanup-hook MANUAL_CLEANUP_HOOK Path or command to execute for the cleanup script (default: None) nginx: Nginx Web Server plugin --nginx-server-root NGINX_SERVER_ROOT Nginx server root directory. (default: /etc/nginx or /usr/local/etc/nginx) --nginx-ctl NGINX_CTL Path to the 'nginx' binary, used for 'configtest' and retrieving nginx version number. (default: nginx) --nginx-sleep-seconds NGINX_SLEEP_SECONDS Number of seconds to wait for nginx configuration changes to apply when reloading. (default: 1) null: Null Installer standalone: Runs an HTTP server locally which serves the necessary validation files under the /.well-known/acme-challenge/ request path. Suitable if there is no HTTP server already running. HTTP challenge only (wildcards not supported). webroot: Saves the necessary validation files to a .well-known/acme-challenge/ directory within the nominated webroot path. A seperate HTTP server must be running and serving files from the webroot path. HTTP challenge only (wildcards not supported). --webroot-path WEBROOT_PATH, -w WEBROOT_PATH public_html / webroot path. This can be specified multiple times to handle different domains; each domain will have the webroot path that preceded it. For instance: `-w /var/www/example -d example.com -d www.example.com -w /var/www/thing -d thing.net -d m.thing.net` (default: Ask) --webroot-map WEBROOT_MAP JSON dictionary mapping domains to webroot paths; this implies -d for each entry. You may need to escape this from your shell. E.g.: --webroot-map '{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' This option is merged with, but takes precedence over, -w / -d entries. At present, if you put webroot-map in a config file, it needs to be on a single line, like: webroot-map = {"example.com":"/var/www"}. (default: {}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/compatibility.rst0000664000175100017510000000372114561227515016526 0ustar00ericaerica======================= Backwards Compatibility ======================= All Certbot components including `acme `_, Certbot, and :ref:`non-third party plugins ` follow `Semantic Versioning `_ both for its Python :doc:`API ` and for the application itself. This means that we will not change behavior in a backwards incompatible way except in a new major version of the project. .. note:: None of this applies to the behavior of Certbot distribution mechanisms such as :ref:`our snaps ` or OS packages whose behavior may change at any time. Semantic versioning only applies to the common Certbot components that are installed by various distribution methods. For Certbot as an application, the command line interface and non-interactive behavior can be considered stable with two exceptions. The first is that no aspects of Certbot's console or log output should be considered stable and it may change at any time. The second is that Certbot's behavior should only be considered stable with certain files but not all. Files with which users should expect Certbot to maintain its current behavior with are: * ``/etc/letsencrypt/live/$domain/{cert,chain,fullchain,privkey}.pem``, where ``$domain`` is the certificate name (see :ref:`where-certs` for more details) * :ref:`CLI configuration files ` * Hook directories in ``/etc/letsencrypt/renewal-hooks`` Certbot's behavior with other files may change at any point. Another area where Certbot should not be considered stable is its behavior when not run in non-interactive mode which also may change at any point. In general, if we're making a change that we expect will break some users, we will bump the major version and will have warned about it in a prior release when possible. For our Python API, we will issue warnings using Python's warning module. For application level changes, we will print and log warning messages. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/conf.py0000664000175100017510000002410314561227515014417 0ustar00ericaerica# -*- coding: utf-8 -*- # # Certbot documentation build configuration file, created by # sphinx-quickstart on Sun Nov 23 20:35:21 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import codecs import os import re import sys import sphinx here = os.path.abspath(os.path.dirname(__file__)) # read version number (and other metadata) from package init init_fn = os.path.join(here, '..', 'certbot', '__init__.py') with codecs.open(init_fn, encoding='utf8') as fd: meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", fd.read())) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.2' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx_rtd_theme', ] if sphinx.version_info >= (1, 6): extensions.append('sphinx.ext.imgconverter') autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Certbot' # this is now overridden by the footer.html template copyright = u'2014-2018 - The Certbot software and documentation are licensed under the Apache 2.0 license as described at https://eff.org/cb-license.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '.'.join(meta['version'].split('.')[:2]) # The full version, including alpha/beta/rc tags. release = meta['version'] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [ '_build', 'challenges.rst', ] # The reST default role (used for this markup: `text`) to use for all # documents. default_role = 'py:obj' # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False suppress_warnings = ['image.nonlocal_uri'] # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'Certbotdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'Certbot.tex', u'Certbot Documentation', u'Certbot Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'certbot', u'Certbot Documentation', [project], 7), ('man/certbot', 'certbot', u"Automatically configure HTTPS using Let's Encrypt", [project], 1), ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Certbot', u'Certbot Documentation', u'Certbot Project', 'Certbot', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/contributing.rst0000664000175100017510000006434114561227515016371 0ustar00ericaerica=============== Developer Guide =============== .. contents:: Table of Contents :local: .. _getting_started: Getting Started =============== Certbot has the same :ref:`system requirements ` when set up for development. While the section below will help you install Certbot and its dependencies, Certbot needs to be run on a UNIX-like OS so if you're using Windows, you'll need to set up a (virtual) machine running an OS such as Linux and continue with these instructions on that UNIX-like OS. .. _local copy: .. _prerequisites: Running a local copy of the client ---------------------------------- Running the client in developer mode from your local tree is a little different than running Certbot as a user. To get set up, clone our git repository by running: .. code-block:: shell git clone https://github.com/certbot/certbot If you're running on a UNIX-like OS, you can run the following commands to install dependencies and set up a virtual environment where you can run Certbot. Install and configure the OS system dependencies required to run Certbot. .. code-block:: shell # For APT-based distributions (e.g. Debian, Ubuntu ...) sudo apt update sudo apt install python3-venv libaugeas0 # For RPM-based distributions (e.g. Fedora, CentOS ...) # NB1: old distributions will use yum instead of dnf # NB2: RHEL-based distributions use python3X instead of python3 (e.g. python38) sudo dnf install python3 augeas-libs # For macOS installations with Homebrew already installed and configured # NB: If you also run `brew install python` you don't need the ~/lib # directory created below, however, Certbot's Apache plugin won't work # if you use Python installed from other sources such as pyenv or the # version provided by Apple. brew install augeas mkdir ~/lib ln -s $(brew --prefix)/lib/libaugeas* ~/lib .. note:: If you have trouble creating the virtual environment below, you may need to install additional dependencies. See the `cryptography project's site`_ for more information. .. _`cryptography project's site`: https://cryptography.io/en/latest/installation.html#building-cryptography-on-linux Set up the Python virtual environment that will host your Certbot local instance. .. code-block:: shell cd certbot python tools/venv.py .. note:: You may need to repeat this when Certbot's dependencies change or when a new plugin is introduced. You can now run the copy of Certbot from git either by executing ``venv/bin/certbot``, or by activating the virtual environment. You can do the latter by running: .. code-block:: shell source venv/bin/activate After running this command, ``certbot`` and development tools like ``ipdb3``, ``ipython``, ``pytest``, and ``tox`` are available in the shell where you ran the command. These tools are installed in the virtual environment and are kept separate from your global Python installation. This works by setting environment variables so the right executables are found and Python can pull in the versions of various packages needed by Certbot. More information can be found in the `virtualenv docs`_. .. _`virtualenv docs`: https://virtualenv.pypa.io Find issues to work on ---------------------- You can find the open issues in the `github issue tracker`_. Comparatively easy ones are marked `good first issue`_. If you're starting work on something, post a comment to let others know and seek feedback on your plan where appropriate. Once you've got a working branch, you can open a pull request. All changes in your pull request must have thorough unit test coverage, pass our tests, and be compliant with the :ref:`coding style `. .. _github issue tracker: https://github.com/certbot/certbot/issues .. _good first issue: https://github.com/certbot/certbot/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22 .. _testing: Testing ------- You can test your code in several ways: - running the `automated unit`_ tests, - running the `automated integration`_ tests - running an *ad hoc* `manual integration`_ test .. note:: Running integration tests does not currently work on macOS. See https://github.com/certbot/certbot/issues/6959. In the meantime, we recommend developers on macOS open a PR to run integration tests. .. _automated unit: Running automated unit tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When you are working in a file ``foo.py``, there should also be a file ``foo_test.py`` either in the same directory as ``foo.py`` or in the ``tests`` subdirectory (if there isn't, make one). While you are working on your code and tests, run ``python foo_test.py`` to run the relevant tests. For debugging, we recommend putting ``import ipdb; ipdb.set_trace()`` statements inside the source code. Once you are done with your code changes, and the tests in ``foo_test.py`` pass, run all of the unit tests for Certbot and check for coverage with ``tox -e cover``. You should then check for code style with ``tox -e lint`` (all files) or ``pylint --rcfile=.pylintrc path/to/file.py`` (single file at a time). Once all of the above is successful, you may run the full test suite using ``tox --skip-missing-interpreters``. We recommend running the commands above first, because running all tests like this is very slow, and the large amount of output can make it hard to find specific failures when they happen. .. warning:: The full test suite may attempt to modify your system's Apache config if your user has sudo permissions, so it should not be run on a production Apache server. .. _automated integration: Running automated integration tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Generally it is sufficient to open a pull request and let Github and Azure Pipelines run integration tests for you. However, you may want to run them locally before submitting your pull request. You need Docker and docker-compose installed and working. The tox environment `integration` will setup `Pebble`_, the Let's Encrypt ACME CA server for integration testing, then launch the Certbot integration tests. With a user allowed to access your local Docker daemon, run: .. code-block:: shell tox -e integration Tests will be run using pytest. A test report and a code coverage report will be displayed at the end of the integration tests execution. .. _Pebble: https://github.com/letsencrypt/pebble .. _manual integration: Running manual integration tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can also manually execute Certbot against a local instance of the `Pebble`_ ACME server. This is useful to verify that the modifications done to the code makes Certbot behave as expected. To do so you need: - Docker installed, and a user with access to the Docker client, - an available `local copy`_ of Certbot. The virtual environment set up with `python tools/venv.py` contains two CLI tools that can be used once the virtual environment is activated: .. code-block:: shell run_acme_server - Starts a local instance of Pebble and runs in the foreground printing its logs. - Press CTRL+C to stop this instance. - This instance is configured to validate challenges against certbot executed locally. .. note:: Some options are available to tweak the local ACME server. You can execute ``run_acme_server --help`` to see the inline help of the ``run_acme_server`` tool. .. code-block:: shell certbot_test [ARGS...] - Execute certbot with the provided arguments and other arguments useful for testing purposes, such as: verbose output, full tracebacks in case Certbot crashes, *etc.* - Execution is preconfigured to interact with the Pebble CA started with ``run_acme_server``. - Any arguments can be passed as they would be to Certbot (eg. ``certbot_test certonly -d test.example.com``). Here is a typical workflow to verify that Certbot successfully issued a certificate using an HTTP-01 challenge on a machine with Python 3: .. code-block:: shell python tools/venv.py source venv/bin/activate run_acme_server & certbot_test certonly --standalone -d test.example.com # To stop Pebble, launch `fg` to get back the background job, then press CTRL+C Running tests in CI ~~~~~~~~~~~~~~~~~~~ Certbot uses Azure Pipelines to run continuous integration tests. If you are using our Azure setup, a branch whose name starts with `test-` will run all tests on that branch. Code components and layout ========================== The following components of the Certbot repository are distributed to users: acme contains all protocol specific code certbot main client code certbot-apache and certbot-nginx client code to configure specific web servers certbot-dns-* client code to configure DNS providers windows installer Installs Certbot on Windows and is built using the files in windows-installer/ Plugin-architecture ------------------- Certbot has a plugin architecture to facilitate support for different webservers, other TLS servers, and operating systems. The interfaces available for plugins to implement are defined in `interfaces.py`_ and `plugins/common.py`_. The main two plugin interfaces are `~certbot.interfaces.Authenticator`, which implements various ways of proving domain control to a certificate authority, and `~certbot.interfaces.Installer`, which configures a server to use a certificate once it is issued. Some plugins, like the built-in Apache and Nginx plugins, implement both interfaces and perform both tasks. Others, like the built-in Standalone authenticator, implement just one interface. .. _interfaces.py: https://github.com/certbot/certbot/blob/master/certbot/certbot/interfaces.py .. _plugins/common.py: https://github.com/certbot/certbot/blob/master/certbot/certbot/plugins/common.py#L45 Authenticators -------------- Authenticators are plugins that prove control of a domain name by solving a challenge provided by the ACME server. ACME currently defines several types of challenges: HTTP, TLS-ALPN, and DNS, represented by classes in `acme.challenges`. An authenticator plugin should implement support for at least one challenge type. An Authenticator indicates which challenges it supports by implementing `get_chall_pref(domain)` to return a sorted list of challenge types in preference order. An Authenticator must also implement `perform(achalls)`, which "performs" a list of challenges by, for instance, provisioning a file on an HTTP server, or setting a TXT record in DNS. Once all challenges have succeeded or failed, Certbot will call the plugin's `cleanup(achalls)` method to remove any files or DNS records that were needed only during authentication. Installer --------- Installers plugins exist to actually setup the certificate in a server, possibly tweak the security configuration to make it more correct and secure (Fix some mixed content problems, turn on HSTS, redirect to HTTPS, etc). Installer plugins tell the main client about their abilities to do the latter via the :meth:`~.Installer.supported_enhancements` call. We currently have two Installers in the tree, the `~.ApacheConfigurator`. and the `~.NginxConfigurator`. External projects have made some progress toward support for IIS, Icecast and Plesk. Installers and Authenticators will oftentimes be the same class/object (because for instance both tasks can be performed by a webserver like nginx) though this is not always the case (the standalone plugin is an authenticator that listens on port 80, but it cannot install certificates; a postfix plugin would be an installer but not an authenticator). Installers and Authenticators are kept separate because it should be possible to use the `~.StandaloneAuthenticator` (it sets up its own Python server to perform challenges) with a program that cannot solve challenges itself (Such as MTA installers). Installer Development --------------------- There are a few existing classes that may be beneficial while developing a new `~certbot.interfaces.Installer`. Installers aimed to reconfigure UNIX servers may use Augeas for configuration parsing and can inherit from `~.AugeasConfigurator` class to handle much of the interface. Installers that are unable to use Augeas may still find the `~.Reverter` class helpful in handling configuration checkpoints and rollback. .. _dev-plugin: Writing your own plugin ----------------------- .. note:: The Certbot team is not currently accepting any new plugins because we want to rethink our approach to the challenge and resolve some issues like `#6464 `_, `#6503 `_, and `#6504 `_ first. In the meantime, you're welcome to release it as a third-party plugin. See `certbot-dns-ispconfig `_ for one example of that. Certbot client supports dynamic discovery of plugins through the `importlib.metadata entry points`_ using the `certbot.plugins` group. This way you can, for example, create a custom implementation of `~certbot.interfaces.Authenticator` or the `~certbot.interfaces.Installer` without having to merge it with the core upstream source code. An example is provided in ``examples/plugins/`` directory. While developing, you can install your plugin into a Certbot development virtualenv like this: .. code-block:: shell . venv/bin/activate pip install -e examples/plugins/ certbot_test plugins Your plugin should show up in the output of the last command. If not, it was not installed properly. Once you've finished your plugin and published it, you can have your users install it system-wide with `pip install`. Note that this will only work for users who have Certbot installed from OS packages or via pip. .. _`importlib.metadata entry points`: https://importlib-metadata.readthedocs.io/en/latest/using.html#entry-points Writing your own plugin snap ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you'd like your plugin to be used alongside the Certbot snap, you will also have to publish your plugin as a snap. Plugin snaps are regular confined snaps, but normally do not provide any "apps" themselves. Plugin snaps export loadable Python modules to the Certbot snap. When the Certbot snap runs, it will use its version of Python and prefer Python modules contained in its own snap over modules contained in external snaps. This means that your snap doesn't have to contain things like an extra copy of Python, Certbot, or their dependencies, but also that if you need a different version of a dependency than is already installed in the Certbot snap, the Certbot snap will have to be updated. Certbot plugin snaps expose their Python modules to the Certbot snap via a `snap content interface`_ where ``certbot-1`` is the value for the ``content`` attribute. The Certbot snap only uses this to find the names of connected plugin snaps and it expects to find the Python modules to be loaded under ``lib/python3.8/site-packages/`` in the plugin snap. This location is the default when using the ``core20`` `base snap`_ and the `python snapcraft plugin`_. The Certbot snap also provides a separate content interface which you can use to get metadata about the Certbot snap using the ``content`` identifier ``metadata-1``. The script used to generate the snapcraft.yaml files for our own externally snapped plugins can be found at https://github.com/certbot/certbot/blob/master/tools/snap/generate_dnsplugins_snapcraft.sh. For more information on building externally snapped plugins, see the section on :ref:`Building snaps`. Once you have created your own snap, if you have the snap file locally, it can be installed for use with Certbot by running: .. code-block:: shell snap install --classic certbot snap set certbot trust-plugin-with-root=ok snap install --dangerous your-snap-filename.snap sudo snap connect certbot:plugin your-snap-name sudo /snap/bin/certbot plugins If everything worked, the last command should list your plugin in the list of plugins found by Certbot. Once your snap is published to the snap store, it will be installable through the name of the snap on the snap store without the ``--dangerous`` flag. If you are also using Certbot's metadata interface, you can run ``sudo snap connect your-snap-name:your-plug-name-for-metadata certbot:certbot-metadata`` to connect your snap to it. .. _`snap content interface`: https://snapcraft.io/docs/content-interface .. _`base snap`: https://snapcraft.io/docs/base-snaps .. _`python snapcraft plugin`: https://snapcraft.io/docs/python-plugin .. _coding-style: Coding style ============ Please: 1. **Be consistent with the rest of the code**. 2. Read `PEP 8 - Style Guide for Python Code`_. 3. Follow the `Google Python Style Guide`_, with the exception that we use `Sphinx-style`_ documentation:: def foo(arg): """Short description. :param int arg: Some number. :returns: Argument :rtype: int """ return arg 4. Remember to use ``pylint``. 5. You may consider installing a plugin for `editorconfig`_ in your editor to prevent some linting warnings. 6. Please avoid `unittest.assertTrue` or `unittest.assertFalse` when possible, and use `assertEqual` or more specific assert. They give better messages when it's failing, and are generally more correct. .. _Google Python Style Guide: https://google.github.io/styleguide/pyguide.html .. _Sphinx-style: https://www.sphinx-doc.org/ .. _PEP 8 - Style Guide for Python Code: https://www.python.org/dev/peps/pep-0008 .. _editorconfig: https://editorconfig.org/ Use ``certbot.compat.os`` instead of ``os`` =========================================== Python's standard library ``os`` module lacks full support for several Windows security features about file permissions (eg. DACLs). However several files handled by Certbot (eg. private keys) need strongly restricted access on both Linux and Windows. To help with this, the ``certbot.compat.os`` module wraps the standard ``os`` module, and forbids usage of methods that lack support for these Windows security features. As a developer, when working on Certbot or its plugins, you must use ``certbot.compat.os`` in every place you would need ``os`` (eg. ``from certbot.compat import os`` instead of ``import os``). Otherwise the tests will fail when your PR is submitted. .. _type annotations: Mypy type annotations ===================== Certbot uses the `mypy`_ static type checker. Python 3 natively supports official type annotations, which can then be tested for consistency using mypy. Mypy does some type checks even without type annotations; we can find bugs in Certbot even without a fully annotated codebase. Zulip wrote a `great guide`_ to using mypy. It’s useful, but you don’t have to read the whole thing to start contributing to Certbot. To run mypy on Certbot, use ``tox -e mypy`` on a machine that has Python 3 installed. Also note that OpenSSL, which we rely on, has type definitions for crypto but not SSL. We use both. Those imports should look like this: .. code-block:: python from OpenSSL import crypto from OpenSSL import SSL .. _mypy: https://mypy.readthedocs.io .. _added in comments: https://mypy.readthedocs.io/en/latest/cheat_sheet.html .. _great guide: https://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/ Submitting a pull request ========================= Steps: 0. We recommend you talk with us in a GitHub issue or :ref:`Mattermost ` before writing a pull request to ensure the changes you're making is something we have the time and interest to review. 1. Write your code! When doing this, you should add :ref:`mypy type annotations ` for any functions you add or modify. You can check that you've done this correctly by running ``tox -e mypy`` on a machine that has Python 3 installed. 2. Make sure your environment is set up properly and that you're in your virtualenv. You can do this by following the instructions in the :ref:`Getting Started ` section. 3. Run ``tox -e lint`` to check for pylint errors. Fix any errors. 4. Run ``tox --skip-missing-interpreters`` to run all the tests we recommend developers run locally. The ``--skip-missing-interpreters`` argument ignores missing versions of Python needed for running the tests. Fix any errors. 5. If any documentation should be added or updated as part of the changes you have made, please include the documentation changes in your PR. 6. Submit the PR. Once your PR is open, please do not force push to the branch containing your pull request to squash or amend commits. We use `squash merges `_ on PRs and rewriting commits makes changes harder to track between reviews. 7. Did your tests pass on Azure Pipelines? If they didn't, fix any errors. .. _ask for help: Asking for help =============== If you have any questions while working on a Certbot issue, don't hesitate to ask for help! You can do this in the Certbot channel in EFF's Mattermost instance for its open source projects as described below. You can get involved with several of EFF's software projects such as Certbot at the `EFF Open Source Contributor Chat Platform `_. By signing up for the EFF Open Source Contributor Chat Platform, you consent to share your personal information with the Electronic Frontier Foundation, which is the operator and data controller for this platform. The channels will be available both to EFF, and to other users of EFFOSCCP, who may use or disclose information in these channels outside of EFFOSCCP. EFF will use your information, according to the `Privacy Policy `_, to further the mission of EFF, including hosting and moderating the discussions on this platform. Use of EFFOSCCP is subject to the `EFF Code of Conduct `_. When investigating an alleged Code of Conduct violation, EFF may review discussion channels or direct messages. .. _Building snaps: Building the Certbot and DNS plugin snaps ========================================= Instructions for how to manually build and run the Certbot snap and the externally snapped DNS plugins that the Certbot project supplies are located in the README file at https://github.com/certbot/certbot/tree/master/tools/snap. Updating the documentation ========================== Many of the packages in the Certbot repository have documentation in a ``docs/`` directory. This directory is located under the top level directory for the package. For instance, Certbot's documentation is under ``certbot/docs``. To build the documentation of a package, make sure you have followed the instructions to set up a `local copy`_ of Certbot including activating the virtual environment. After that, ``cd`` to the docs directory you want to build and run the command: .. code-block:: shell make clean html This would generate the HTML documentation in ``_build/html`` in your current ``docs/`` directory. Certbot's dependencies ====================== We attempt to pin all of Certbot's dependencies whenever we can for reliability and consistency. Some of the places we have Certbot's dependencies pinned include our snaps, Docker images, Windows installer, CI, and our development environments. In most cases, the file where dependency versions are specified is ``tools/requirements.txt``. The one exception to this is our "oldest" tests where ``tools/oldest_constraints.txt`` is used instead. The purpose of the "oldest" tests is to ensure Certbot continues to work with the oldest versions of our dependencies which we claim to support. The oldest versions of the dependencies we support should also be declared in our setup.py files to communicate this information to our users. The choices of whether Certbot's dependencies are pinned and what file is used if they are should be automatically handled for you most of the time by Certbot's tooling. The way it works though is ``tools/pip_install.py`` (which many of our other tools build on) checks for the presence of environment variables. If ``CERTBOT_OLDEST`` is set to 1, ``tools/oldest_constraints.txt`` will be used as constraints for ``pip``, otherwise, ``tools/requirements.txt`` is used as constraints. Updating dependency versions ---------------------------- ``tools/requirements.txt`` and ``tools/oldest_constraints.txt`` can be updated using ``tools/pinning/current/repin.sh`` and ``tools/pinning/oldest/repin.sh`` respectively. This works by using ``poetry`` to generate pinnings based on a Poetry project defined by the ``pyproject.toml`` file in the same directory as the script. In many cases, you can just run the script to generate updated dependencies, however, if you need to pin back packages or unpin packages that were previously restricted to an older version, you will need to modify the ``pyproject.toml`` file. The syntax used by this file is described at https://python-poetry.org/docs/pyproject/ and how dependencies are specified in this file is further described at https://python-poetry.org/docs/dependency-specification/. If you want to learn more about the design used here, see ``tools/pinning/DESIGN.md`` in the Certbot repo. Choosing dependency versions ---------------------------- A number of Unix distributions create third-party Certbot packages for their users. Where feasible, the Certbot project tries to manage its dependencies in a way that does not create avoidable work for packagers. Avoiding adding new dependencies is a good way to help with this. When adding new or upgrading existing Python dependencies, Certbot developers should pay attention to which distributions are actively packaging Certbot. In particular: - EPEL (used by RHEL/CentOS/Fedora) updates Certbot regularly. At the time of writing, EPEL9 is the release of EPEL where Certbot is being updated, but check the `EPEL home page `_ and `pkgs.org `_ for the latest release. - Debian and Ubuntu only package Certbot when making new releases of their distros. Checking the available version of dependencies in Debian "sid" and "unstable" can help to identify dependencies that are likely to be available in the next stable release of these distros. If a dependency is already packaged in these distros and is acceptable for use in Certbot, the oldest packaged version of that dependency should be chosen and set as the minimum version in ``setup.py``. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/index.rst0000664000175100017510000000053314561227515014762 0ustar00ericaericaWelcome to the Certbot documentation! ================================================== .. toctree:: :maxdepth: 2 intro what install using contributing packaging compatibility resources .. toctree:: :maxdepth: 1 api Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/install.rst0000664000175100017510000001345714561227515015332 0ustar00ericaerica===================== Get Certbot ===================== .. contents:: Table of Contents :local: .. _system_requirements: System Requirements ------------------- - Linux, macOS, BSD and Windows - Recommended root access on Linux/BSD/Required Administrator access on Windows - Port 80 Open .. Note:: Certbot is most useful when run with root privileges, because it is then able to automatically configure TLS/SSL for Apache and nginx. \ *Certbot is meant to be run directly on a web server*, normally by a system administrator. In most cases, running Certbot on your personal computer is not a useful option. The instructions below relate to installing and running Certbot on a server. Installation ------------ Unless you have very specific requirements, we kindly suggest that you use the installation instructions for your system found at https://certbot.eff.org/instructions. .. _snap-install: Snap (Recommended) ------------------ Our instructions are the same across all systems that use Snap. You can find instructions for installing Certbot through Snap can be found at https://certbot.eff.org/instructions by selecting your server software and then choosing "snapd" in the "System" dropdown menu. Most modern Linux distributions (basically any that use systemd) can install Certbot packaged as a snap. Snaps are available for x86_64, ARMv7 and ARMv8 architectures. The Certbot snap provides an easy way to ensure you have the latest version of Certbot with features like automated certificate renewal preconfigured. If you unable to use snaps, you can use an alternate method for installing ``certbot``. .. _docker-user: Alternative 1: Docker --------------------- Docker_ is an amazingly simple and quick way to obtain a certificate. However, this mode of operation is unable to install certificates or configure your webserver, because our installer plugins cannot reach your webserver from inside the Docker container. Most users should use the instructions at certbot.eff.org_. You should only use Docker if you are sure you know what you are doing and have a good reason to do so. You should definitely read the :ref:`where-certs` section, in order to know how to manage the certificates manually. `Our ciphersuites page `__ provides some information about recommended ciphersuites. If none of these make much sense to you, you should definitely use the installation method recommended for your system at certbot.eff.org_, which enables you to use installer plugins that cover both of those hard topics. If you're still not convinced and have decided to use this method, from the server that the domain you're requesting a certificate for resolves to, `install Docker`_, then issue a command like the one found below. If you are using Certbot with the :ref:`Standalone` plugin, you will need to make the port it uses accessible from outside of the container by including something like ``-p 80:80`` or ``-p 443:443`` on the command line before ``certbot/certbot``. .. code-block:: shell sudo docker run -it --rm --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ certbot/certbot certonly Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory ``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from within Docker, you must install the certificate manually according to the procedure recommended by the provider of your webserver. There are also Docker images for each of Certbot's DNS plugins available at https://hub.docker.com/u/certbot which automate doing domain validation over DNS for popular providers. To use one, just replace ``certbot/certbot`` in the command above with the name of the image you want to use. For example, to use Certbot's plugin for Amazon Route 53, you'd use ``certbot/dns-route53``. You may also need to add flags to Certbot and/or mount additional directories to provide access to your DNS API credentials as specified in the :ref:`DNS plugin documentation `. For more information about the layout of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. .. _Docker: https://docker.com .. _`install Docker`: https://docs.docker.com/engine/installation/ .. _certbot.eff.org: https://certbot.eff.org/instructions .. _pip: Alternative 2: Pip ------------------ Installing Certbot through pip is only supported on a best effort basis and when using a virtual environment. Instructions for installing Certbot through pip can be found at https://certbot.eff.org/instructions by selecting your server software and then choosing "pip" in the "System" dropdown menu. .. _third-party: Alternative 3: Third Party Distributions ---------------------------------------- Third party distributions exist for other specific needs. They often are maintained by these parties outside of Certbot and tend to rapidly fall out of date on LTS-style distributions. .. _certbot-auto: Certbot-Auto [Deprecated] ------------------------- .. toctree:: :hidden: We used to have a shell script named ``certbot-auto`` to help people install Certbot on UNIX operating systems, however, this script is no longer supported. Please remove ``certbot-auto``. To do so, you need to do three things: 1. If you added a cron job or systemd timer to automatically run certbot-auto to renew your certificates, you should delete it. If you did this by following our instructions, you can delete the entry added to `/etc/crontab` by running a command like `sudo sed -i '/certbot-auto/d' /etc/crontab`. 2. Delete the certbot-auto script. If you placed it in `/usr/local/bin`` like we recommended, you can delete it by running `sudo rm /usr/local/bin/certbot-auto`. 3. Delete the Certbot installation created by certbot-auto by running `sudo rm -rf /opt/eff.org`. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/intro.rst0000664000175100017510000000040214561227515015001 0ustar00ericaerica===================== Introduction ===================== .. note:: To get started quickly, use the `interactive installation guide `_. .. include:: ../README.rst :start-after: tag:intro-begin :end-before: tag:intro-end ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/make.bat0000664000175100017510000001613314561227515014531 0ustar00ericaerica@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\LetsEncrypt.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\LetsEncrypt.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3430836 certbot-2.9.0/docs/man/0000775000175100017510000000000014561227516013674 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/man/certbot.rst0000664000175100017510000000135714561227515016075 0ustar00ericaerica:orphan: ======= certbot ======= Synopsis ======== The objective of Certbot, Let's Encrypt, and the ACME (Automated Certificate Management Environment) protocol is to make it possible to set up an HTTPS server and have it automatically obtain a browser-trusted certificate, without any human intervention. This is accomplished by running a certificate management agent on the web server. This agent is used to: - Automatically prove to the Let's Encrypt CA that you control the website - Obtain a browser-trusted certificate and set it up on your web server - Keep track of when your certificate is going to expire, and renew it - Help you revoke the certificate if that ever becomes necessary. Options ======= .. literalinclude:: ../cli-help.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/packaging.rst0000664000175100017510000000545514561227515015607 0ustar00ericaerica=============== Packaging Guide =============== Releases ======== We release packages and upload them to PyPI (wheels and source tarballs). - https://pypi.org/project/acme/ - https://pypi.org/project/certbot/ - https://pypi.org/project/certbot-apache/ - https://pypi.org/project/certbot-nginx/ - https://pypi.org/project/certbot-dns-cloudflare/ - https://pypi.org/project/certbot-dns-digitalocean/ - https://pypi.org/project/certbot-dns-dnsimple/ - https://pypi.org/project/certbot-dns-dnsmadeeasy/ - https://pypi.org/project/certbot-dns-google/ - https://pypi.org/project/certbot-dns-linode/ - https://pypi.org/project/certbot-dns-luadns/ - https://pypi.org/project/certbot-dns-nsone/ - https://pypi.org/project/certbot-dns-ovh/ - https://pypi.org/project/certbot-dns-rfc2136/ - https://pypi.org/project/certbot-dns-route53/ The following scripts are used in the process: - https://github.com/certbot/certbot/blob/master/tools/release.sh We use git tags to identify releases, using `Semantic Versioning`_. For example: `v0.11.1`. .. _`Semantic Versioning`: https://semver.org/ Since version 1.21.0, our packages are cryptographically signed by one of four PGP keys: - ``BF6BCFC89E90747B9A680FD7B6029E8500F7DB16`` - ``86379B4F0AF371B50CD9E5FF3402831161D1D280`` - ``20F201346BF8F3F455A73F9A780CC99432A28621`` - ``F2871B4152AE13C49519111F447BF683AA3B26C3``` These keys can be found on major key servers and at https://dl.eff.org/certbot.pub. Releases before 1.21.0 were signed by the PGP key ``A2CFB51FA275A7286234E7B24D17C995CD9775F2`` which can still be found on major key servers. Notes for package maintainers ============================= 0. Please use our tagged releases, not ``master``! 1. Do not package ``certbot-compatibility-test`` as it's only used internally. 2. To run tests on our packages, you should use pytest by running the command ``python -m pytest``. Running ``pytest`` directly may not work because PYTHONPATH is not handled the same way and local modules may not be found by the test runner. 3. If you'd like to include automated renewal in your package: - ``certbot renew -q`` should be added to crontab or systemd timer. - A random per-machine time offset should be included to avoid having a large number of your clients hit Let's Encrypt's servers simultaneously. - ``--preconfigured-renewal`` should be included on the CLI or in ``cli.ini`` for all invocations of Certbot, so that it can adjust its interactive output regarding automated renewal (Certbot >= 1.9.0). 4. ``jws`` is an internal script for ``acme`` module and it doesn't have to be packaged - it's mostly for debugging: you can use it as ``echo foo | jws sign | jws verify``. 5. Do get in touch with us. We are happy to make any changes that will make packaging easier. If you need to apply some patches don't do it downstream - make a PR here. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/resources.rst0000664000175100017510000000022314561227515015661 0ustar00ericaerica===================== Resources ===================== .. include:: ../README.rst :start-after: tag:links-begin :end-before: tag:links-end ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/using.rst0000664000175100017510000016057414561227515015014 0ustar00ericaerica========== User Guide ========== .. contents:: Table of Contents :local: Certbot Commands ================ Certbot uses a number of different commands (also referred to as "subcommands") to request specific actions such as obtaining, renewing, or revoking certificates. The most important and commonly-used commands will be discussed throughout this document; an exhaustive list also appears near the end of the document. The ``certbot`` script on your web server might be named ``letsencrypt`` if your system uses an older package. Throughout the docs, whenever you see ``certbot``, swap in the correct name as needed. .. _plugins: Getting certificates (and choosing plugins) =========================================== Certbot helps you achieve two tasks: 1. Obtaining a certificate: automatically performing the required authentication steps to prove that you control the domain(s), saving the certificate to ``/etc/letsencrypt/live/`` and renewing it on a regular schedule. 2. Optionally, installing that certificate to supported web servers (like Apache or nginx) and other kinds of servers. This is done by automatically modifying the configuration of your server in order to use the certificate. To obtain a certificate and also install it, use the ``certbot run`` command (or ``certbot``, which is the same). To just obtain the certificate without installing it anywhere, the ``certbot certonly`` ("certificate only") command can be used. Some example ways to use Certbot:: # Obtain and install a certificate: certbot # Obtain a certificate but don't install it: certbot certonly # You may specify multiple domains with -d and obtain and # install different certificates by running Certbot multiple times: certbot certonly -d example.com -d www.example.com certbot certonly -d app.example.com -d api.example.com To perform these tasks, Certbot will ask you to choose from a selection of authenticator and installer plugins. The appropriate choice of plugins will depend on what kind of server software you are running and plan to use your certificates with. **Authenticators** are plugins which automatically perform the required steps to prove that you control the domain names you're trying to request a certificate for. An authenticator is always required to obtain a certificate. **Installers** are plugins which can automatically modify your web server's configuration to serve your website over HTTPS, using the certificates obtained by Certbot. An installer is only required if you want Certbot to install the certificate to your web server. Some plugins are both authenticators and installers and it is possible to specify a distinct combination_ of authenticator and plugin. =========== ==== ==== =============================================================== ============================= Plugin Auth Inst Notes Challenge types (and port) =========== ==== ==== =============================================================== ============================= apache_ Y Y | Automates obtaining and installing a certificate with Apache. http-01_ (80) nginx_ Y Y | Automates obtaining and installing a certificate with Nginx. http-01_ (80) webroot_ Y N | Obtains a certificate by writing to the webroot directory of http-01_ (80) | an already running webserver. standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. http-01_ (80) | Requires port 80 to be available. This is useful on | systems with no webserver, or when direct integration with | the local webserver is not supported or not desired. |dns_plugs| Y N | This category of plugins automates obtaining a certificate by dns-01_ (53) | modifying DNS records to prove you have control over a | domain. Doing domain validation in this way is | the only way to obtain wildcard certificates from Let's | Encrypt. manual_ Y N | Obtain a certificate by manually following instructions to http-01_ (80) or | perform domain validation yourself. Certificates created this dns-01_ (53) | way do not support autorenewal. | Autorenewal may be enabled by providing an authentication | hook script to automate the domain validation steps. =========== ==== ==== =============================================================== ============================= .. |dns_plugs| replace:: :ref:`DNS plugins ` Under the hood, plugins use one of several ACME protocol challenges_ to prove you control a domain. The options are http-01_ (which uses port 80) and dns-01_ (requiring configuration of a DNS server on port 53, though that's often not the same machine as your webserver). A few plugins support more than one challenge type, in which case you can choose one with ``--preferred-challenges``. There are also many third-party-plugins_ available. Below we describe in more detail the circumstances in which each plugin can be used, and how to use it. .. _challenges: https://datatracker.ietf.org/doc/html/rfc8555#section-8 .. _http-01: https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 .. _dns-01: https://datatracker.ietf.org/doc/html/rfc8555#section-8.4 Apache ------ The Apache plugin currently `supports `_ modern OSes based on Debian, Fedora, SUSE, Gentoo, CentOS and Darwin. This automates both obtaining *and* installing certificates on an Apache webserver. To specify this plugin on the command line, simply include ``--apache``. Webroot ------- If you're running a local webserver for which you have the ability to modify the content being served, and you'd prefer not to stop the webserver during the certificate issuance process, you can use the webroot plugin to obtain a certificate by including ``certonly`` and ``--webroot`` on the command line. In addition, you'll need to specify ``--webroot-path`` or ``-w`` with the top-level directory ("web root") containing the files served by your webserver. For example, ``--webroot-path /var/www/html`` or ``--webroot-path /usr/share/nginx/html`` are two common webroot paths. If you're getting a certificate for many domains at once, the plugin needs to know where each domain's files are served from, which could potentially be a separate directory for each domain. When requesting a certificate for multiple domains, each domain will use the most recently specified ``--webroot-path``. So, for instance, :: certbot certonly --webroot -w /var/www/example -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net would obtain a single certificate for all of those names, using the ``/var/www/example`` webroot directory for the first two, and ``/var/www/other`` for the second two. The webroot plugin works by creating a temporary file for each of your requested domains in ``${webroot-path}/.well-known/acme-challenge``. Then the Let's Encrypt validation server makes HTTP requests to validate that the DNS for each requested domain resolves to the server running certbot. An example request made to your web server would look like: :: 66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] "GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" Note that to use the webroot plugin, your server must be configured to serve files from hidden directories. If ``/.well-known`` is treated specially by your webserver configuration, you might need to modify the configuration to ensure that files inside ``/.well-known/acme-challenge`` are served by the webserver. Under Windows, Certbot will generate a ``web.config`` file, if one does not already exist, in ``/.well-known/acme-challenge`` in order to let IIS serve the challenge files even if they do not have an extension. Nginx ----- The Nginx plugin should work for most configurations. We recommend backing up Nginx configurations before using it (though you can also revert changes to configurations with ``certbot --nginx rollback``). You can use it by providing the ``--nginx`` flag on the commandline. :: certbot --nginx .. _standalone: Standalone ---------- Use standalone mode to obtain a certificate if you don't want to use (or don't currently have) existing server software. The standalone plugin does not rely on any other server software running on the machine where you obtain the certificate. To obtain a certificate using a "standalone" webserver, you can use the standalone plugin by including ``certonly`` and ``--standalone`` on the command line. This plugin needs to bind to port 80 in order to perform domain validation, so you may need to stop your existing webserver. It must still be possible for your machine to accept inbound connections from the Internet on the specified port using each requested domain name. By default, Certbot first attempts to bind to the port for all interfaces using IPv6 and then bind to that port using IPv4; Certbot continues so long as at least one bind succeeds. On most Linux systems, IPv4 traffic will be routed to the bound IPv6 port and the failure during the second bind is expected. Use ``---address`` to explicitly tell Certbot which interface (and protocol) to bind. .. _dns_plugins: DNS Plugins ----------- If you'd like to obtain a wildcard certificate from Let's Encrypt or run ``certbot`` on a machine other than your target webserver, you can use one of Certbot's DNS plugins. These plugins are not included in a default Certbot installation and must be installed separately. They are available in many OS package managers, as Docker images, and as snaps. Visit https://certbot.eff.org to learn the best way to use the DNS plugins on your system. Once installed, you can find documentation on how to use each plugin at: * `certbot-dns-cloudflare `_ * `certbot-dns-digitalocean `_ * `certbot-dns-dnsimple `_ * `certbot-dns-dnsmadeeasy `_ * `certbot-dns-gehirn `_ * `certbot-dns-google `_ * `certbot-dns-linode `_ * `certbot-dns-luadns `_ * `certbot-dns-nsone `_ * `certbot-dns-ovh `_ * `certbot-dns-rfc2136 `_ * `certbot-dns-route53 `_ * `certbot-dns-sakuracloud `_ Manual ------ If you'd like to obtain a certificate running ``certbot`` on a machine other than your target webserver or perform the steps for domain validation yourself, you can use the manual plugin. While hidden from the UI, you can use the plugin to obtain a certificate by specifying ``certonly`` and ``--manual`` on the command line. This requires you to copy and paste commands into another terminal session, which may be on a different computer. The manual plugin can use either the ``http`` or the ``dns`` challenge. You can use the ``--preferred-challenges`` option to choose the challenge of your preference. The ``http`` challenge will ask you to place a file with a specific name and specific content in the ``/.well-known/acme-challenge/`` directory directly in the top-level directory (“web rootâ€) containing the files served by your webserver. In essence it's the same as the webroot_ plugin, but not automated. When using the ``dns`` challenge, ``certbot`` will ask you to place a TXT DNS record with specific contents under the domain name consisting of the hostname for which you want a certificate issued, prepended by ``_acme-challenge``. For example, for the domain ``example.com``, a zone file entry would look like: :: _acme-challenge.example.com. 300 IN TXT "gfj9Xq...Rg85nM" .. _manual-renewal: **Renewal with the manual plugin** Certificates created using ``--manual`` **do not** support automatic renewal unless combined with an `authentication hook script <#hooks>`_ via ``--manual-auth-hook`` to automatically set up the required HTTP and/or TXT challenges. If you can use one of the other plugins_ which support autorenewal to create your certificate, doing so is highly recommended. To manually renew a certificate using ``--manual`` without hooks, repeat the same ``certbot --manual`` command you used to create the certificate originally. As this will require you to copy and paste new HTTP files or DNS TXT records, the command cannot be automated with a cron job. .. _combination: Combining plugins ----------------- Sometimes you may want to specify a combination of distinct authenticator and installer plugins. To do so, specify the authenticator plugin with ``--authenticator`` or ``-a`` and the installer plugin with ``--installer`` or ``-i``. For instance, you could create a certificate using the webroot_ plugin for authentication and the apache_ plugin for installation. :: certbot run -a webroot -i apache -w /var/www/html -d example.com Or you could create a certificate using the manual_ plugin for authentication and the nginx_ plugin for installation. (Note that this certificate cannot be renewed automatically.) :: certbot run -a manual -i nginx -d example.com .. _third-party-plugins: Third-party plugins ------------------- There are also a number of third-party plugins for the client, provided by other developers. Many are beta/experimental, but some are already in widespread use: ================== ==== ==== =============================================================== Plugin Auth Inst Notes ================== ==== ==== =============================================================== haproxy_ Y Y Integration with the HAProxy load balancer s3front_ Y Y Integration with Amazon CloudFront distribution of S3 buckets gandi_ Y N Obtain certificates via the Gandi LiveDNS API varnish_ Y N Obtain certificates via a Varnish server external-auth_ Y Y A plugin for convenient scripting pritunl_ N Y Install certificates in pritunl distributed OpenVPN servers proxmox_ N Y Install certificates in Proxmox Virtualization servers dns-standalone_ Y N Obtain certificates via an integrated DNS server dns-ispconfig_ Y N DNS Authentication using ISPConfig as DNS server dns-clouddns_ Y N DNS Authentication using CloudDNS API dns-lightsail_ Y N DNS Authentication using Amazon Lightsail DNS API dns-inwx_ Y Y DNS Authentication for INWX through the XML API dns-azure_ Y N DNS Authentication using Azure DNS dns-godaddy_ Y N DNS Authentication using Godaddy DNS dns-yandexcloud_ Y N DNS Authentication using Yandex Cloud DNS dns-bunny_ Y N DNS Authentication using BunnyDNS njalla_ Y N DNS Authentication for njalla DuckDNS_ Y N DNS Authentication for DuckDNS Porkbun_ Y N DNS Authentication for Porkbun Infomaniak_ Y N DNS Authentication using Infomaniak Domains API dns-multi_ Y N DNS authentication of 100+ providers using go-acme/lego dns-dnsmanager_ Y N DNS Authentication for dnsmanager.io standalone-nfq_ Y N HTTP Authentication that works with any webserver (Linux only) dns-solidserver_ Y N DNS Authentication using SOLIDserver (EfficientIP) dns-stackit_ Y N DNS Authentication using STACKIT DNS ================== ==== ==== =============================================================== .. _haproxy: https://github.com/greenhost/certbot-haproxy .. _s3front: https://github.com/dlapiduz/letsencrypt-s3front .. _gandi: https://github.com/obynio/certbot-plugin-gandi .. _varnish: https://git.sesse.net/?p=letsencrypt-varnish-plugin .. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl .. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox .. _external-auth: https://github.com/EnigmaBridge/certbot-external-auth .. _dns-standalone: https://github.com/siilike/certbot-dns-standalone .. _dns-ispconfig: https://github.com/m42e/certbot-dns-ispconfig .. _dns-clouddns: https://github.com/vshosting/certbot-dns-clouddns .. _dns-lightsail: https://github.com/noi/certbot-dns-lightsail .. _dns-inwx: https://github.com/oGGy990/certbot-dns-inwx/ .. _dns-azure: https://github.com/binkhq/certbot-dns-azure .. _dns-godaddy: https://github.com/miigotu/certbot-dns-godaddy .. _dns-yandexcloud: https://github.com/PykupeJIbc/certbot-dns-yandexcloud .. _dns-bunny: https://github.com/mwt/certbot-dns-bunny .. _njalla: https://github.com/chaptergy/certbot-dns-njalla .. _DuckDNS: https://github.com/infinityofspace/certbot_dns_duckdns .. _Porkbun: https://github.com/infinityofspace/certbot_dns_porkbun .. _Infomaniak: https://github.com/Infomaniak/certbot-dns-infomaniak .. _dns-multi: https://github.com/alexzorin/certbot-dns-multi .. _dns-dnsmanager: https://github.com/stayallive/certbot-dns-dnsmanager .. _standalone-nfq: https://github.com/alexzorin/certbot-standalone-nfq .. _dns-solidserver: https://gitlab.com/charlyhong/certbot-dns-solidserver .. _dns-stackit: https://github.com/stackitcloud/certbot-dns-stackit If you're interested, you can also :ref:`write your own plugin `. .. _managing-certs: Managing certificates ===================== To view a list of the certificates Certbot knows about, run the ``certificates`` subcommand: ``certbot certificates`` This returns information in the following format:: Found the following certificates: Certificate Name: example.com Domains: example.com, www.example.com Expiry Date: 2017-02-19 19:53:00+00:00 (VALID: 30 days) Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem Key Type: RSA Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem ``Certificate Name`` shows the name of the certificate. Pass this name using the ``--cert-name`` flag to specify a particular certificate for the ``run``, ``certonly``, ``certificates``, ``renew``, and ``delete`` commands. The certificate name cannot contain filepath separators (i.e. '/' or '\\', depending on the platform). Example:: certbot certonly --cert-name example.com .. _updating_certs: Re-creating and Updating Existing Certificates ---------------------------------------------- You can use ``certonly`` or ``run`` subcommands to request the creation of a single new certificate even if you already have an existing certificate with some of the same domain names. If a certificate is requested with ``run`` or ``certonly`` specifying a certificate name that already exists, Certbot updates the existing certificate. Otherwise a new certificate is created and assigned the specified name. The ``--force-renewal``, ``--duplicate``, and ``--expand`` options control Certbot's behavior when re-creating a certificate with the same name as an existing certificate. If you don't specify a requested behavior, Certbot may ask you what you intended. ``--force-renewal`` tells Certbot to request a new certificate with the same domains as an existing certificate. Each domain must be explicitly specified via ``-d``. If successful, this certificate is saved alongside the earlier one and symbolic links (the "``live``" reference) will be updated to point to the new certificate. This is a valid method of renewing a specific individual certificate. ``--duplicate`` tells Certbot to create a separate, unrelated certificate with the same domains as an existing certificate. This certificate is saved completely separately from the prior one. Most users will not need to issue this command in normal circumstances. ``--expand`` tells Certbot to update an existing certificate with a new certificate that contains all of the old domains and one or more additional new domains. With the ``--expand`` option, use the ``-d`` option to specify all existing domains and one or more new domains. Example: .. code-block:: none certbot --expand -d existing.com,example.com,newdomain.com If you prefer, you can specify the domains individually like this: .. code-block:: none certbot --expand -d existing.com -d example.com -d newdomain.com Consider using ``--cert-name`` instead of ``--expand``, as it gives more control over which certificate is modified and it lets you remove domains as well as adding them. ``--allow-subset-of-names`` tells Certbot to continue with certificate generation if only some of the specified domain authorizations can be obtained. This may be useful if some domains specified in a certificate no longer point at this system. Whenever you obtain a new certificate in any of these ways, the new certificate exists alongside any previously obtained certificates, whether or not the previous certificates have expired. The generation of a new certificate counts against several rate limits that are intended to prevent abuse of the ACME protocol, as described `here `__. .. _changing: Changing a Certificate's Domains -------------------------------- The ``--cert-name`` flag can also be used to modify the domains a certificate contains, by specifying new domains using the ``-d`` or ``--domains`` flag. If certificate ``example.com`` previously contained ``example.com`` and ``www.example.com``, it can be modified to only contain ``example.com`` by specifying only ``example.com`` with the ``-d`` or ``--domains`` flag. Example:: certbot certonly --cert-name example.com -d example.com The same format can be used to expand the set of domains a certificate contains, or to replace that set entirely:: certbot certonly --cert-name example.com -d example.org,www.example.org .. _using-ecdsa-keys: RSA and ECDSA keys ------------------------ Certbot supports two certificate private key algorithms: ``rsa`` and ``ecdsa``. As of version 2.0.0, Certbot defaults to ECDSA ``secp256r1`` (P-256) certificate private keys for all new certificates. Existing certificates will continue to renew using their existing key type, unless a key type change is requested. The type of key used by Certbot can be controlled through the ``--key-type`` option. You can use the ``--elliptic-curve`` option to control the curve used in ECDSA certificates and the ``--rsa-key-size`` option to control the size of RSA keys. .. warning:: If you obtain certificates using ECDSA keys, you should be careful not to downgrade to a Certbot version earlier than 1.10.0 where ECDSA keys were not supported. Downgrades like this are possible if you switch from something like the snaps or pip to packages provided by your operating system which often lag behind. Changing a certificate's key type ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Unless you are aware that you need to support very old HTTPS clients that are not supported by most sites, you can safely transition your site to use ECDSA keys instead of RSA keys. If you want to change a single certificate to use ECDSA keys, you'll need to create or renew a certificate while setting ``--key-type ecdsa`` on the command line: .. code-block:: shell certbot renew --key-type ecdsa --cert-name example.com --force-renewal If you want to use ECDSA keys for all certificates in the future (including renewals of existing certificates), you can add the following line to Certbot's :ref:`configuration file `: .. code-block:: ini key-type = ecdsa which will take effect upon the next renewal of each certificate. Revoking certificates --------------------- If you need to revoke a certificate, use the ``revoke`` subcommand to do so. A certificate may be revoked by providing its name (see ``certbot certificates``) or by providing its path directly:: certbot revoke --cert-name example.com certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem If the certificate being revoked was obtained via the ``--staging``, ``--test-cert`` or a non-default ``--server`` flag, that flag must be passed to the ``revoke`` subcommand. .. note:: After revocation, Certbot will (by default) ask whether you want to **delete** the certificate. Unless deleted, Certbot will try to renew revoked certificates the next time ``certbot renew`` runs. You can also specify the reason for revoking your certificate by using the ``reason`` flag. Reasons include ``unspecified`` which is the default, as well as ``keycompromise``, ``affiliationchanged``, ``superseded``, and ``cessationofoperation``:: certbot revoke --cert-name example.com --reason keycompromise Revoking by account key or certificate private key ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, Certbot will try revoke the certificate using your ACME account key. If the certificate was created from the same ACME account, the revocation will be successful. If you instead have the corresponding private key file to the certificate you wish to revoke, use ``--key-path`` to perform the revocation from any ACME account:: certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem --key-path /etc/letsencrypt/live/example.com/privkey.pem .. _deleting: Deleting certificates --------------------- If you need to delete a certificate, use the ``delete`` subcommand. .. note:: Read this and the `Safely deleting certificates`_ sections carefully. This is an irreversible operation and must be done with care. Certbot does not automatically revoke a certificate before deleting it. If you're no longer using a certificate and don't plan to use it anywhere else, you may want to follow the instructions in `Revoking certificates`_ instead. Generally, there's no need to revoke a certificate if its private key has not been compromised, but you may still receive expiration emails from Let's Encrypt unless you revoke. .. note:: Do not manually delete certificate files from inside ``/etc/letsencrypt/``. Always use the ``delete`` subcommand. A certificate may be deleted by providing its name with ``--cert-name``. \ You may find its name using ``certbot certificates``. Otherwise, you will be prompted to choose one or more certificates to delete:: certbot delete --cert-name example.com # or to choose from a list: certbot delete Safely deleting certificates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Deleting a certificate without following the proper steps can result in a non-functioning server. To safely delete a certificate, follow all the steps below to make sure that references to a certificate are removed from the configuration of any installed server software (Apache, nginx, Postfix, etc) *before* deleting the certificate. To explain further, when installing a certificate, Certbot modifies Apache or nginx's configuration to load the certificate and its private key from the ``/etc/letsencrypt/live/`` directory. Before deleting a certificate, it is necessary to undo that modification, by removing any references to the certificate from the webserver's configuration files. Follow these steps to safely delete a certificate: 1. Find all references to the certificate (substitute ``example.com`` in the command for the name of the certificate you wish to delete):: sudo bash -c 'grep -R live/example.com /etc/{nginx,httpd,apache2}' If there are no references found, skip directly to Step 4. If some references are found, they will look something like:: /etc/apache2/sites-available/000-default-le-ssl.conf:SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem /etc/apache2/sites-available/000-default-le-ssl.conf:SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem 2. You will need a self-signed certificate to replace the certificate you are deleting. The following command will generate one for you, saving the certificate at ``/etc/letsencrypt/self-signed-cert.pem`` and its private key at ``/etc/letsencrypt/self-signed-privkey.pem``:: sudo openssl req -nodes -batch -x509 -newkey rsa:2048 -keyout /etc/letsencrypt/self-signed-privkey.pem -out /etc/letsencrypt/self-signed-cert.pem -days 356 3. For each reference found in Step 1, open the file in a text editor and replace the reference to the existing certificate with a reference to the self-signed certificate. Continuing from the previous example, you would open ``/etc/apache2/sites-available/000-default-le-ssl.conf`` in a text editor and modify the two matching lines of text to instead say:: SSLCertificateFile /etc/letsencrypt/self-signed-cert.pem SSLCertificateKeyFile /etc/letsencrypt/self-signed-privkey.pem 4. It is now safe to delete the certificate. Do so by running:: sudo certbot delete --cert-name example.com .. _renewal: Renewing certificates --------------------- .. note:: Let's Encrypt CA issues short-lived certificates (90 days). Make sure you renew the certificates at least once in 3 months. .. seealso:: Most Certbot installations come with automatic renewal out of the box. See `Automated Renewals`_ for more details. .. seealso:: Users of the `Manual`_ plugin should note that ``--manual`` certificates will not renew automatically, unless combined with authentication hook scripts. See `Renewal with the manual plugin <#manual-renewal>`_. As of version 0.10.0, Certbot supports a ``renew`` action to check all installed certificates for impending expiry and attempt to renew them. The simplest form is simply ``certbot renew`` This command attempts to renew any previously-obtained certificates that expire in less than 30 days. The same plugin and options that were used at the time the certificate was originally issued will be used for the renewal attempt, unless you specify other plugins or options. Unlike ``certonly``, ``renew`` acts on multiple certificates and always takes into account whether each one is near expiry. Because of this, ``renew`` is suitable (and designed) for automated use, to allow your system to automatically renew each certificate when appropriate. Since ``renew`` only renews certificates that are near expiry it can be run as frequently as you want - since it will usually take no action. The ``renew`` command includes hooks for running commands or scripts before or after a certificate is renewed. For example, if you have a single certificate obtained using the standalone_ plugin, you might need to stop the webserver before renewing so standalone can bind to the necessary ports, and then restart it after the plugin is finished. Example:: certbot renew --pre-hook "service nginx stop" --post-hook "service nginx start" If a hook exits with a non-zero exit code, the error will be printed to ``stderr`` but renewal will be attempted anyway. A failing hook doesn't directly cause Certbot to exit with a non-zero exit code, but since Certbot exits with a non-zero exit code when renewals fail, a failed hook causing renewal failures will indirectly result in a non-zero exit code. Hooks will only be run if a certificate is due for renewal, so you can run the above command frequently without unnecessarily stopping your webserver. When Certbot detects that a certificate is due for renewal, ``--pre-hook`` and ``--post-hook`` hooks run before and after each attempt to renew it. If you want your hook to run only after a successful renewal, use ``--deploy-hook`` in a command like this. ``certbot renew --deploy-hook /path/to/deploy-hook-script`` You can also specify hooks by placing files in subdirectories of Certbot's configuration directory. Assuming your configuration directory is ``/etc/letsencrypt``, any executable files found in ``/etc/letsencrypt/renewal-hooks/pre``, ``/etc/letsencrypt/renewal-hooks/deploy``, and ``/etc/letsencrypt/renewal-hooks/post`` will be run as pre, deploy, and post hooks respectively when any certificate is renewed with the ``renew`` subcommand. These hooks are run in alphabetical order and are not run for other subcommands. (The order the hooks are run is determined by the byte value of the characters in their filenames and is not dependent on your locale.) Hooks specified in the command line, :ref:`configuration file `, or :ref:`renewal configuration files ` are run as usual after running all hooks in these directories. One minor exception to this is if a hook specified elsewhere is simply the path to an executable file in the hook directory of the same type (e.g. your pre-hook is the path to an executable in ``/etc/letsencrypt/renewal-hooks/pre``), the file is not run a second time. You can stop Certbot from automatically running executables found in these directories by including ``--no-directory-hooks`` on the command line. More information about hooks can be found by running ``certbot --help renew``. If you're sure that this command executes successfully without human intervention, you can add the command to ``crontab`` (since certificates are only renewed when they're determined to be near expiry, the command can run on a regular basis, like every week or every day). In that case, you are likely to want to use the ``-q`` or ``--quiet`` quiet flag to silence all output except errors. If you are manually renewing all of your certificates, the ``--force-renewal`` flag may be helpful; it causes the expiration time of the certificate(s) to be ignored when considering renewal, and attempts to renew each and every installed certificate regardless of its age. (This form is not appropriate to run daily because each certificate will be renewed every day, which will quickly run into the certificate authority rate limit.) Starting with Certbot 2.7.0, certbot provides the environment variables `RENEWED_DOMAINS` and `FAILED_DOMAINS` to all post renewal hooks. These variables contain a space separated list of domains. These variables can be used to determine if a renewal has succeeded or failed as part of your post renewal hook. Note that options provided to ``certbot renew`` will apply to *every* certificate for which renewal is attempted; for example, ``certbot renew --rsa-key-size 4096`` would try to replace every near-expiry certificate with an equivalent certificate using a 4096-bit RSA public key. If a certificate is successfully renewed using specified options, those options will be saved and used for future renewals of that certificate. An alternative form that provides for more fine-grained control over the renewal process (while renewing specified certificates one at a time), is ``certbot certonly`` with the complete set of subject domains of a specific certificate specified via `-d` flags. You may also want to include the ``-n`` or ``--noninteractive`` flag to prevent blocking on user input (which is useful when running the command from cron). ``certbot certonly -n -d example.com -d www.example.com`` All of the domains covered by the certificate must be specified in this case in order to renew and replace the old certificate rather than obtaining a new one; don't forget any `www.` domains! Specifying a subset of the domains creates a new, separate certificate containing only those domains, rather than replacing the original certificate. When run with a set of domains corresponding to an existing certificate, the ``certonly`` command attempts to renew that specific certificate. Please note that the CA will send notification emails to the address you provide if you do not renew certificates that are about to expire. Certbot is working hard to improve the renewal process, and we apologize for any inconvenience you encounter in integrating these commands into your individual environment. .. note:: ``certbot renew`` exit status will only be 1 if a renewal attempt failed. This means ``certbot renew`` exit status will be 0 if no certificate needs to be updated. If you write a custom script and expect to run a command only after a certificate was actually renewed you will need to use the ``--deploy-hook`` since the exit status will be 0 both on successful renewal and when renewal is not necessary. .. _renewal-config-file: .. _Modifying the Renewal Configuration File: Modifying the Renewal Configuration of Existing Certificates ------------------------------------------------------------ When creating a certificate, Certbot will keep track of all of the relevant options chosen by the user. At renewal time, Certbot will remember these options and apply them once again. Sometimes, you may encounter the need to change some of these options for future certificate renewals. To achieve this, you will need to perform the following steps: Certbot v2.3.0 and newer ~~~~~~~~~~~~~~~~~~~~~~~~ The ``certbot reconfigure`` command can be used to change a certificate's renewal options. This command will use the new renewal options to perform a test renewal against the Let's Encrypt staging server. If this is successful, the new renewal options will be saved and will apply to future renewals. You will need to specify the ``--cert-name``, which can be found by running ``certbot certificates``. A list of common options that may be updated with the ``reconfigure`` command can be found by running ``certbot help reconfigure``. As a practical example, if you were using the ``webroot`` authenticator and had relocated your website to another directory, you can change the ``--webroot-path`` to the new directory using the following command: .. code-block:: shell certbot reconfigure --cert-name example.com --webroot-path /path/to/new/location Certbot v2.2.0 and older ~~~~~~~~~~~~~~~~~~~~~~~~ 1. Perform a *dry run renewal* with the amended options on the command line. This allows you to confirm that the change is valid and will result in successful future renewals. 2. If the dry run is successful, perform a *live renewal* of the certificate. This will persist the change for future renewals. If the certificate is not yet due to expire, you will need to force a renewal using ``--force-renewal``. .. note:: Rate limits from the certificate authority may prevent you from performing multiple renewals in a short period of time. It is strongly recommended to perform the second step only once, when you have decided on what options should change. As a practical example, if you were using the ``webroot`` authenticator and had relocated your website to another directory, you would need to change the ``--webroot-path`` to the new directory. Following the above advice: 1. Perform a *dry-run renewal* of the individual certificate with the amended options:: certbot renew --cert-name example.com --webroot-path /path/to/new/location --dry-run 2. If the dry-run was successful, make the change permanent by performing a *live renewal* of the certificate with the amended options, including ``--force-renewal``:: certbot renew --cert-name example.com --webroot-path /path/to/new/location --force-renewal ``--cert-name`` selects the particular certificate to be modified. Without this option, all certificates will be selected. ``--webroot-path`` is the option intended to be changed. All other previously selected options will be kept the same and do not need to be included in the command. For advanced certificate management tasks, it is also possible to manually modify the certificate's renewal configuration file, but this is discouraged since it can easily break Certbot's ability to renew your certificates. These renewal configuration files are located at ``/etc/letsencrypt/renewal/CERTNAME.conf``. If you choose to modify the renewal configuration file we advise you to make a backup of the file beforehand and test its validity with the ``certbot renew --dry-run`` command. .. warning:: Manually modifying files under ``/etc/letsencrypt/renewal/`` can damage them if done improperly and we do not recommend doing so. Automated Renewals ------------------ Most Certbot installations come with automatic renewals preconfigured. This is done by means of a scheduled task which runs ``certbot renew`` periodically. If you are unsure whether you need to configure automated renewal: 1. Review the instructions for your system and installation method at https://certbot.eff.org/instructions. They will describe how to set up a scheduled task, if necessary. If no step is listed, your system comes with automated renewal pre-installed, and you should not need to take any additional actions. 2. On Linux and BSD, you can check to see if your installation method has pre-installed a timer for you. To do so, look for the ``certbot renew`` command in either your system's crontab (typically `/etc/crontab` or `/etc/cron.*/*`) or systemd timers (``systemctl list-timers``). 3. If you're still not sure, you can configure automated renewal manually by following the steps in the next section. Certbot has been carefully engineered to handle the case where both manual automated renewal and pre-installed automated renewal are set up. Setting up automated renewal ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you think you may need to set up automated renewal, follow these instructions to set up a scheduled task to automatically renew your certificates in the background. If you are unsure whether your system has a pre-installed scheduled task for Certbot, it is safe to follow these instructions to create one. .. note:: If you're using Windows, these instructions are not neccessary as Certbot on Windows comes with a scheduled task for automated renewal pre-installed. If you are using macOS and installed Certbot using Homebrew, follow the instructions at https://certbot.eff.org/instructions to set up automated renewal. The instructions below are not applicable on macOS. Run the following line, which will add a cron job to `/etc/crontab`: .. code-block:: shell SLEEPTIME=$(awk 'BEGIN{srand(); print int(rand()*(3600+1))}'); echo "0 0,12 * * * root sleep $SLEEPTIME && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null If you needed to stop your webserver to run Certbot, you'll want to add ``pre`` and ``post`` hooks to stop and start your webserver automatically. For example, if your webserver is HAProxy, run the following commands to create the hook files in the appropriate directory: .. code-block:: shell sudo sh -c 'printf "#!/bin/sh\nservice haproxy stop\n" > /etc/letsencrypt/renewal-hooks/pre/haproxy.sh' sudo sh -c 'printf "#!/bin/sh\nservice haproxy start\n" > /etc/letsencrypt/renewal-hooks/post/haproxy.sh' sudo chmod 755 /etc/letsencrypt/renewal-hooks/pre/haproxy.sh sudo chmod 755 /etc/letsencrypt/renewal-hooks/post/haproxy.sh Congratulations, Certbot will now automatically renew your certificates in the background. If you are interested in learning more about how Certbot renews your certificates, see the `Renewing certificates`_ section above. .. _where-certs: Where are my certificates? ========================== All generated keys and issued certificates can be found in ``/etc/letsencrypt/live/$domain``, where ``$domain`` is the certificate name (see the note below). Rather than copying, please point your (web) server configuration directly to those files (or create symlinks). During the renewal_, ``/etc/letsencrypt/live`` is updated with the latest necessary files. .. note:: The certificate name ``$domain`` used in the path ``/etc/letsencrypt/live/$domain`` follows this convention: * it is the name given to ``--cert-name``, * if ``--cert-name`` is not set by the user it is the first domain given to ``--domains``, * if the first domain is a wildcard domain (eg. ``*.example.com``) the certificate name will be ``example.com``, * if a name collision would occur with a certificate already named ``example.com``, the new certificate name will be constructed using a numerical sequence as ``example.com-001``. For historical reasons, the containing directories are created with permissions of ``0700`` meaning that certificates are accessible only to servers that run as the root user. **If you will never downgrade to an older version of Certbot**, then you can safely fix this using ``chmod 0755 /etc/letsencrypt/{live,archive}``. For servers that drop root privileges before attempting to read the private key file, you will also need to use ``chgrp`` and ``chmod 0640`` to allow the server to read ``/etc/letsencrypt/live/$domain/privkey.pem``. The following files are available: ``privkey.pem`` Private key for the certificate. .. warning:: This **must be kept secret at all times**! Never share it with anyone, including Certbot developers. You cannot put it into a safe, however - your server still needs to access this file in order for SSL/TLS to work. .. note:: As of Certbot version 0.29.0, private keys for new certificate default to ``0600``. Any changes to the group mode or group owner (gid) of this file will be preserved on renewals. This is what Apache needs for `SSLCertificateKeyFile `_, and Nginx for `ssl_certificate_key `_. ``fullchain.pem`` All certificates, **including** server certificate (aka leaf certificate or end-entity certificate). The server certificate is the first one in this file, followed by any intermediates. This is what Apache >= 2.4.8 needs for `SSLCertificateFile `_, and what Nginx needs for `ssl_certificate `_. ``cert.pem`` and ``chain.pem`` (less common) ``cert.pem`` contains the server certificate by itself, and ``chain.pem`` contains the additional intermediate certificate or certificates that web browsers will need in order to validate the server certificate. If you provide one of these files to your web server, you **must** provide both of them, or some browsers will show "This Connection is Untrusted" errors for your site, `some of the time `_. Apache < 2.4.8 needs these for `SSLCertificateFile `_. and `SSLCertificateChainFile `_, respectively. If you're using OCSP stapling with Nginx >= 1.3.7, ``chain.pem`` should be provided as the `ssl_trusted_certificate `_ to validate OCSP responses. .. note:: All files are PEM-encoded. If you need other format, such as DER or PFX, then you could convert using ``openssl``. You can automate that with ``--deploy-hook`` if you're using automatic renewal_. .. _hooks: Pre and Post Validation Hooks ============================= Certbot allows for the specification of pre and post validation hooks when run in manual mode. The flags to specify these scripts are ``--manual-auth-hook`` and ``--manual-cleanup-hook`` respectively and can be used as follows: :: certbot certonly --manual --manual-auth-hook /path/to/http/authenticator.sh --manual-cleanup-hook /path/to/http/cleanup.sh -d secure.example.com This will run the ``authenticator.sh`` script, attempt the validation, and then run the ``cleanup.sh`` script. Additionally certbot will pass relevant environment variables to these scripts: - ``CERTBOT_DOMAIN``: The domain being authenticated - ``CERTBOT_VALIDATION``: The validation string - ``CERTBOT_TOKEN``: Resource name part of the HTTP-01 challenge (HTTP-01 only) - ``CERTBOT_REMAINING_CHALLENGES``: Number of challenges remaining after the current challenge - ``CERTBOT_ALL_DOMAINS``: A comma-separated list of all domains challenged for the current certificate Additionally for cleanup: - ``CERTBOT_AUTH_OUTPUT``: Whatever the auth script wrote to stdout Example usage for HTTP-01: :: certbot certonly --manual --preferred-challenges=http --manual-auth-hook /path/to/http/authenticator.sh --manual-cleanup-hook /path/to/http/cleanup.sh -d secure.example.com /path/to/http/authenticator.sh .. code-block:: none #!/bin/bash echo $CERTBOT_VALIDATION > /var/www/htdocs/.well-known/acme-challenge/$CERTBOT_TOKEN /path/to/http/cleanup.sh .. code-block:: none #!/bin/bash rm -f /var/www/htdocs/.well-known/acme-challenge/$CERTBOT_TOKEN Example usage for DNS-01 (Cloudflare API v4) (for example purposes only, do not use as-is) :: certbot certonly --manual --preferred-challenges=dns --manual-auth-hook /path/to/dns/authenticator.sh --manual-cleanup-hook /path/to/dns/cleanup.sh -d secure.example.com /path/to/dns/authenticator.sh .. code-block:: none #!/bin/bash # Get your API key from https://www.cloudflare.com/a/account/my-account API_KEY="your-api-key" EMAIL="your.email@example.com" # Strip only the top domain to get the zone id DOMAIN=$(expr match "$CERTBOT_DOMAIN" '.*\.\(.*\..*\)') # Get the Cloudflare zone id ZONE_EXTRA_PARAMS="status=active&page=1&per_page=20&order=status&direction=desc&match=all" ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN&$ZONE_EXTRA_PARAMS" \ -H "X-Auth-Email: $EMAIL" \ -H "X-Auth-Key: $API_KEY" \ -H "Content-Type: application/json" | python -c "import sys,json;print(json.load(sys.stdin)['result'][0]['id'])") # Create TXT record CREATE_DOMAIN="_acme-challenge.$CERTBOT_DOMAIN" RECORD_ID=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \ -H "X-Auth-Email: $EMAIL" \ -H "X-Auth-Key: $API_KEY" \ -H "Content-Type: application/json" \ --data '{"type":"TXT","name":"'"$CREATE_DOMAIN"'","content":"'"$CERTBOT_VALIDATION"'","ttl":120}' \ | python -c "import sys,json;print(json.load(sys.stdin)['result']['id'])") # Save info for cleanup if [ ! -d /tmp/CERTBOT_$CERTBOT_DOMAIN ];then mkdir -m 0700 /tmp/CERTBOT_$CERTBOT_DOMAIN fi echo $ZONE_ID > /tmp/CERTBOT_$CERTBOT_DOMAIN/ZONE_ID echo $RECORD_ID > /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID # Sleep to make sure the change has time to propagate over to DNS sleep 25 /path/to/dns/cleanup.sh .. code-block:: none #!/bin/bash # Get your API key from https://www.cloudflare.com/a/account/my-account API_KEY="your-api-key" EMAIL="your.email@example.com" if [ -f /tmp/CERTBOT_$CERTBOT_DOMAIN/ZONE_ID ]; then ZONE_ID=$(cat /tmp/CERTBOT_$CERTBOT_DOMAIN/ZONE_ID) rm -f /tmp/CERTBOT_$CERTBOT_DOMAIN/ZONE_ID fi if [ -f /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID ]; then RECORD_ID=$(cat /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID) rm -f /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID fi # Remove the challenge TXT record from the zone if [ -n "${ZONE_ID}" ]; then if [ -n "${RECORD_ID}" ]; then curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \ -H "X-Auth-Email: $EMAIL" \ -H "X-Auth-Key: $API_KEY" \ -H "Content-Type: application/json" fi fi .. _lock-files: Changing the ACME Server ======================== By default, Certbot uses Let's Encrypt's production server at https://acme-v02.api.letsencrypt.org/directory. You can tell Certbot to use a different CA by providing ``--server`` on the command line or in a :ref:`configuration file ` with the URL of the server's ACME directory. For example, if you would like to use Let's Encrypt's staging server, you would add ``--server https://acme-staging-v02.api.letsencrypt.org/directory`` to the command line. .. note:: ``--dry-run`` uses the Let's Encrypt staging server, unless ``--server`` is specified on the CLI or in the :ref:`cli.ini configuration file `. Take caution when using ``--dry-run`` with a custom server, as it may cause real certificates to be issued and discarded. If Certbot does not trust the SSL certificate used by the ACME server, you can use the `REQUESTS_CA_BUNDLE `_ environment variable to override the root certificates trusted by Certbot. Certbot uses the ``requests`` library, which does not use the operating system trusted root store. Make sure that ``REQUESTS_CA_BUNDLE`` is set globally in the environment and not only on the CLI, or scheduled renewal will not succeed. Lock Files ========== When processing a validation Certbot writes a number of lock files on your system to prevent multiple instances from overwriting each other's changes. This means that by default two instances of Certbot will not be able to run in parallel. Since the directories used by Certbot are configurable, Certbot will write a lock file for all of the directories it uses. This include Certbot's ``--work-dir``, ``--logs-dir``, and ``--config-dir``. By default these are ``/var/lib/letsencrypt``, ``/var/log/letsencrypt``, and ``/etc/letsencrypt`` respectively. Additionally if you are using Certbot with Apache or nginx it will lock the configuration folder for that program, which are typically also in the ``/etc`` directory. Note that these lock files will only prevent other instances of Certbot from using those directories, not other processes. If you'd like to run multiple instances of Certbot simultaneously you should specify different directories as the ``--work-dir``, ``--logs-dir``, and ``--config-dir`` for each instance of Certbot that you would like to run. .. _config-file: Configuration file ================== Certbot accepts a global configuration file that applies its options to all invocations of Certbot. Certificate specific configuration choices should be set in the ``.conf`` files that can be found in ``/etc/letsencrypt/renewal``. By default no cli.ini file is created (though it may exist already if you installed Certbot via a package manager, for instance). After creating one it is possible to specify the location of this configuration file with ``certbot --config cli.ini`` (or shorter ``-c cli.ini``). An example configuration file is shown below: .. include:: ../examples/cli.ini :code: ini By default, the following locations are searched: - ``/etc/letsencrypt/cli.ini`` - ``$XDG_CONFIG_HOME/letsencrypt/cli.ini`` (or ``~/.config/letsencrypt/cli.ini`` if ``$XDG_CONFIG_HOME`` is not set). Since this configuration file applies to all invocations of certbot it is incorrect to list domains in it. Listing domains in cli.ini may prevent renewal from working. Additionally due to how arguments in cli.ini are parsed, options which wish to not be set should not be listed. Options set to false will instead be read as being set to true by older versions of Certbot, since they have been listed in the config file. .. keep it up to date with constants.py .. _log-rotation: Log Rotation ============ By default certbot stores status logs in ``/var/log/letsencrypt``. By default certbot will begin rotating logs once there are 1000 logs in the log directory. Meaning that once 1000 files are in ``/var/log/letsencrypt`` Certbot will delete the oldest one to make room for new logs. The number of subsequent logs can be changed by passing the desired number to the command line flag ``--max-log-backups``. Setting this flag to 0 disables log rotation entirely, causing certbot to always append to the same log file. .. note:: Some distributions, including Debian and Ubuntu, disable certbot's internal log rotation in favor of a more traditional logrotate script. If you are using a distribution's packages and want to alter the log rotation, check `/etc/logrotate.d/` for a certbot rotation script. .. _command-line: Certbot command-line options ============================ Certbot supports a lot of command line options. Here's the full list, from ``certbot --help all``: .. literalinclude:: cli-help.txt Getting help ============ If you're having problems, we recommend posting on the Let's Encrypt `Community Forum `_. If you find a bug in the software, please do report it in our `issue tracker `_. Remember to give us as much information as possible: - copy and paste exact command line used and the output (though mind that the latter might include some personally identifiable information, including your email and domains) - copy and paste logs from ``/var/log/letsencrypt`` (though mind they also might contain personally identifiable information) - copy and paste ``certbot --version`` output - your operating system, including specific version - specify which installation method you've chosen ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/docs/what.rst0000664000175100017510000000326014561227515014616 0ustar00ericaerica====================== What is a Certificate? ====================== A public key or digital *certificate* (formerly called an SSL certificate) uses a public key and a private key to enable secure communication between a client program (web browser, email client, etc.) and a server over an encrypted SSL (secure socket layer) or TLS (transport layer security) connection. The certificate is used both to encrypt the initial stage of communication (secure key exchange) and to identify the server. The certificate includes information about the key, information about the server identity, and the digital signature of the certificate issuer. If the issuer is trusted by the software that initiates the communication, and the signature is valid, then the key can be used to communicate securely with the server identified by the certificate. Using a certificate is a good way to prevent "man-in-the-middle" attacks, in which someone in between you and the server you think you are talking to is able to insert their own (harmful) content. You can use Certbot to easily obtain and configure a free certificate from Let's Encrypt, a joint project of EFF, Mozilla, and many other sponsors. Certificates and Lineages ========================= Certbot introduces the concept of a *lineage,* which is a collection of all the versions of a certificate plus Certbot configuration information maintained for that certificate from renewal to renewal. Whenever you renew a certificate, Certbot keeps the same configuration unless you explicitly change it, for example by adding or removing domains. If you add domains, you can either add them to an existing lineage or create a new one. See also: :ref:`updating_certs` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3430836 certbot-2.9.0/examples/0000775000175100017510000000000014561227516014007 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/examples/.gitignore0000664000175100017510000000004414561227515015774 0ustar00ericaerica# generate-csr.sh: /key.pem /csr.der././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/examples/cli.ini0000664000175100017510000000240314561227515015255 0ustar00ericaerica# This is an example of the kind of things you can do in a configuration file. # All flags used by the client can be configured here. Run Certbot with # "--help" to learn more about the available options. # # Note that these options apply automatically to all use of Certbot for # obtaining or renewing certificates, so options specific to a single # certificate on a system with several certificates should not be placed # here. # Use ECC for the private key key-type = ecdsa elliptic-curve = secp384r1 # Use a 4096 bit RSA key instead of 2048 rsa-key-size = 4096 # Uncomment and update to register with the specified e-mail address # email = foo@example.com # Uncomment to use the standalone authenticator on port 443 # authenticator = standalone # Uncomment to use the webroot authenticator. Replace webroot-path with the # path to the public_html / webroot folder being served by your web server. # authenticator = webroot # webroot-path = /usr/share/nginx/html # Uncomment to automatically agree to the terms of service of the ACME server # agree-tos = true # An example of using an alternate ACME server that uses EAB credentials # server = https://acme.sectigo.com/v2/InCommonRSAOV # eab-kid = somestringofstuffwithoutquotes # eab-hmac-key = yaddayaddahexhexnotquoted ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/examples/dev-cli.ini0000664000175100017510000000070514561227515016034 0ustar00ericaerica# Always use the staging/testing server - avoids rate limiting server = https://acme-staging-v02.api.letsencrypt.org/directory # This is an example configuration file for developers config-dir = /tmp/le/conf work-dir = /tmp/le/conf logs-dir = /tmp/le/logs # make sure to use a valid email and domains! email = foo@example.com domains = example.com text = True agree-tos = True debug = True verbose-level = 2 # -vv (debug) authenticator = standalone ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/examples/generate-csr.sh0000775000175100017510000000132114561227515016721 0ustar00ericaerica#!/bin/sh # This script generates a simple SAN CSR to be used with Let's Encrypt # CA. Mostly intended for "auth --csr" testing, but, since it's easily # auditable, feel free to adjust it and use it on your production web # server. if [ "$#" -lt 1 ] then echo "Usage: $0 domain [domain...]" >&2 exit 1 fi domains="DNS:$1" shift for x in "$@" do domains="$domains,DNS:$x" done SAN="$domains" openssl req -config "${OPENSSL_CNF:-openssl.cnf}" \ -new -nodes -subj '/' -reqexts san \ -out "${CSR_PATH:-csr.der}" \ -keyout "${KEY_PATH:-key.pem}" \ -newkey rsa:2048 \ -outform DER # 512 or 1024 too low for Boulder, 2048 is smallest for tests echo "You can now run: certbot auth --csr ${CSR_PATH:-csr.der}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/examples/openssl.cnf0000664000175100017510000000016214561227515016160 0ustar00ericaerica[ req ] distinguished_name = req_distinguished_name [ req_distinguished_name ] [ san ] subjectAltName=${ENV::SAN} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3430836 certbot-2.9.0/examples/plugins/0000775000175100017510000000000014561227516015470 5ustar00ericaerica././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/examples/plugins/certbot_example_plugins.py0000664000175100017510000000124214561227515022756 0ustar00ericaerica"""Example Certbot plugins. For full examples, see `certbot.plugins`. """ from certbot import interfaces from certbot.plugins import common class Authenticator(common.Plugin, interfaces.Authenticator): """Example Authenticator.""" description = "Example Authenticator plugin" # Implement all methods from Authenticator, remembering to add # "self" as first argument, e.g. def prepare(self)... class Installer(common.Plugin, interfaces.Installer): """Example Installer.""" description = "Example Installer plugin" # Implement all methods from Installer, remembering to add # "self" as first argument, e.g. def get_all_names(self)... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/examples/plugins/setup.py0000664000175100017510000000057214561227515017205 0ustar00ericaericafrom setuptools import setup setup( name='certbot-example-plugins', package='certbot_example_plugins.py', install_requires=[ 'certbot', ], entry_points={ 'certbot.plugins': [ 'example_authenticator = certbot_example_plugins:Authenticator', 'example_installer = certbot_example_plugins:Installer', ], }, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707421518.3470836 certbot-2.9.0/setup.cfg0000664000175100017510000000004614561227516014012 0ustar00ericaerica[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707421517.0 certbot-2.9.0/setup.py0000664000175100017510000001077014561227515013707 0ustar00ericaericaimport codecs import os import re from setuptools import find_packages from setuptools import setup def read_file(filename, encoding='utf8'): """Read unicode from given file.""" with codecs.open(filename, encoding=encoding) as fd: return fd.read() here = os.path.abspath(os.path.dirname(__file__)) # read version number (and other metadata) from package init init_fn = os.path.join(here, 'certbot', '__init__.py') meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", read_file(init_fn))) readme = read_file(os.path.join(here, 'README.rst')) version = meta['version'] # This package relies on PyOpenSSL and requests, however, it isn't specified # here to avoid masking the more specific request requirements in acme. See # https://github.com/pypa/pip/issues/988 for more info. install_requires = [ # We specify the minimum acme version as the current Certbot version for # simplicity. See https://github.com/certbot/certbot/issues/8761 for more # info. f'acme>={version}', 'ConfigArgParse>=1.5.3', 'configobj>=5.0.6', 'cryptography>=3.2.1', 'distro>=1.0.1', 'importlib_resources>=1.3.1; python_version < "3.9"', 'importlib_metadata>=4.6; python_version < "3.10"', 'josepy>=1.13.0', 'parsedatetime>=2.4', 'pyrfc3339', 'pytz>=2019.3', # This dependency needs to be added using environment markers to avoid its # installation on Linux. 'pywin32>=300 ; sys_platform == "win32"', 'setuptools>=41.6.0', ] dev_extras = [ 'azure-devops', 'ipdb', # poetry 1.2.0+ is required for it to pin pip, setuptools, and wheel. See # https://github.com/python-poetry/poetry/issues/1584. 'poetry>=1.2.0', # poetry-plugin-export>=1.1.0 is required to use the constraints.txt export # format. See # https://github.com/python-poetry/poetry-plugin-export/blob/efcfd34859e72f6a79a80398f197ce6eb2bbd7cd/CHANGELOG.md#added. 'poetry-plugin-export>=1.1.0', 'twine', ] docs_extras = [ # If you have Sphinx<1.5.1, you need docutils<0.13.1 # https://github.com/sphinx-doc/sphinx/issues/3212 'Sphinx>=1.2', # Annotation support 'sphinx_rtd_theme', ] # Tools like pip, wheel, and tox are listed here to ensure they are properly # pinned and installed during automated testing. test_extras = [ 'coverage', 'mypy', 'pip', 'pylint', 'pytest', 'pytest-cov', 'pytest-xdist', 'setuptools', 'tox', 'types-httplib2', 'types-pyOpenSSL', 'types-pyRFC3339', 'types-pytz', 'types-pywin32', 'types-requests', 'types-setuptools', 'types-six', 'wheel', ] all_extras = dev_extras + docs_extras + test_extras setup( name='certbot', version=version, description="ACME client", long_description=readme, url='https://github.com/certbot/certbot', author="Certbot Project", author_email='certbot-dev@eff.org', license='Apache License 2.0', python_requires='>=3.8', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: Console :: Curses', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', 'Topic :: System :: Networking', 'Topic :: System :: Systems Administration', 'Topic :: Utilities', ], packages=find_packages(exclude=['docs', 'examples', 'tests', 'venv']), include_package_data=True, install_requires=install_requires, extras_require={ 'all': all_extras, 'dev': dev_extras, 'docs': docs_extras, 'test': test_extras, }, entry_points={ 'console_scripts': [ 'certbot = certbot.main:main', ], 'certbot.plugins': [ 'manual = certbot._internal.plugins.manual:Authenticator', 'null = certbot._internal.plugins.null:Installer', 'standalone = certbot._internal.plugins.standalone:Authenticator', 'webroot = certbot._internal.plugins.webroot:Authenticator', ], }, )