Version in base suite: 0.10.2-1 Base version: python-certbot_0.10.2-1 Target version: python-certbot_0.28.0-1~deb9u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-certbot/python-certbot_0.10.2-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-certbot/python-certbot_0.28.0-1~deb9u1.dsc /srv/release.debian.org/tmp/5tQRbMTy6D/python-certbot-0.28.0/certbot/tests/testdata/cert.der |binary /srv/release.debian.org/tmp/5tQRbMTy6D/python-certbot-0.28.0/certbot/tests/testdata/csr-san.der |binary /srv/release.debian.org/tmp/5tQRbMTy6D/python-certbot-0.28.0/certbot/tests/testdata/csr.der |binary /srv/release.debian.org/tmp/5tQRbMTy6D/python-certbot-0.28.0/certbot/tests/testdata/csr_512.der |binary python-certbot-0.28.0/CHANGELOG.md | 1284 ++++++++++ python-certbot-0.28.0/CHANGES.rst | 8 python-certbot-0.28.0/MANIFEST.in | 3 python-certbot-0.28.0/PKG-INFO | 61 python-certbot-0.28.0/README.rst | 45 python-certbot-0.28.0/certbot.egg-info/PKG-INFO | 61 python-certbot-0.28.0/certbot.egg-info/SOURCES.txt | 77 python-certbot-0.28.0/certbot.egg-info/requires.txt | 24 python-certbot-0.28.0/certbot/__init__.py | 2 python-certbot-0.28.0/certbot/account.py | 195 + python-certbot-0.28.0/certbot/achallenges.py | 6 python-certbot-0.28.0/certbot/auth_handler.py | 283 +- python-certbot-0.28.0/certbot/cert_manager.py | 292 +- python-certbot-0.28.0/certbot/cli.py | 671 +++-- python-certbot-0.28.0/certbot/client.py | 365 +- python-certbot-0.28.0/certbot/colored_logging.py | 45 python-certbot-0.28.0/certbot/compat.py | 174 + python-certbot-0.28.0/certbot/configuration.py | 35 python-certbot-0.28.0/certbot/constants.py | 152 + python-certbot-0.28.0/certbot/crypto_util.py | 367 +- python-certbot-0.28.0/certbot/display/completer.py | 6 python-certbot-0.28.0/certbot/display/enhancements.py | 8 python-certbot-0.28.0/certbot/display/ops.py | 97 python-certbot-0.28.0/certbot/display/util.py | 97 python-certbot-0.28.0/certbot/eff.py | 98 python-certbot-0.28.0/certbot/error_handler.py | 58 python-certbot-0.28.0/certbot/errors.py | 12 python-certbot-0.28.0/certbot/hooks.py | 189 + python-certbot-0.28.0/certbot/interfaces.py | 136 - python-certbot-0.28.0/certbot/lock.py | 124 python-certbot-0.28.0/certbot/log.py | 353 ++ python-certbot-0.28.0/certbot/main.py | 1145 ++++++-- python-certbot-0.28.0/certbot/ocsp.py | 6 python-certbot-0.28.0/certbot/plugins/common.py | 250 + python-certbot-0.28.0/certbot/plugins/common_test.py | 249 + python-certbot-0.28.0/certbot/plugins/disco.py | 33 python-certbot-0.28.0/certbot/plugins/disco_test.py | 52 python-certbot-0.28.0/certbot/plugins/dns_common.py | 335 ++ python-certbot-0.28.0/certbot/plugins/dns_common_lexicon.py | 102 python-certbot-0.28.0/certbot/plugins/dns_common_lexicon_test.py | 27 python-certbot-0.28.0/certbot/plugins/dns_common_test.py | 233 + python-certbot-0.28.0/certbot/plugins/dns_test_common.py | 63 python-certbot-0.28.0/certbot/plugins/dns_test_common_lexicon.py | 128 python-certbot-0.28.0/certbot/plugins/enhancements.py | 164 + python-certbot-0.28.0/certbot/plugins/enhancements_test.py | 65 python-certbot-0.28.0/certbot/plugins/manual.py | 138 - python-certbot-0.28.0/certbot/plugins/manual_test.py | 87 python-certbot-0.28.0/certbot/plugins/null_test.py | 3 python-certbot-0.28.0/certbot/plugins/selection.py | 105 python-certbot-0.28.0/certbot/plugins/selection_test.py | 81 python-certbot-0.28.0/certbot/plugins/standalone.py | 277 +- python-certbot-0.28.0/certbot/plugins/standalone_test.py | 189 - python-certbot-0.28.0/certbot/plugins/storage.py | 119 python-certbot-0.28.0/certbot/plugins/storage_test.py | 117 python-certbot-0.28.0/certbot/plugins/util.py | 146 - python-certbot-0.28.0/certbot/plugins/util_test.py | 157 - python-certbot-0.28.0/certbot/plugins/webroot.py | 133 - python-certbot-0.28.0/certbot/plugins/webroot_test.py | 99 python-certbot-0.28.0/certbot/renewal.py | 109 python-certbot-0.28.0/certbot/reporter.py | 21 python-certbot-0.28.0/certbot/reverter.py | 25 python-certbot-0.28.0/certbot/ssl-dhparams.pem | 8 python-certbot-0.28.0/certbot/storage.py | 160 - python-certbot-0.28.0/certbot/tests/account_test.py | 223 + python-certbot-0.28.0/certbot/tests/acme_util.py | 6 python-certbot-0.28.0/certbot/tests/auth_handler_test.py | 312 +- python-certbot-0.28.0/certbot/tests/cert_manager_test.py | 455 ++- python-certbot-0.28.0/certbot/tests/cli_test.py | 203 + python-certbot-0.28.0/certbot/tests/client_test.py | 360 +- python-certbot-0.28.0/certbot/tests/colored_logging_test.py | 41 python-certbot-0.28.0/certbot/tests/compat_test.py | 21 python-certbot-0.28.0/certbot/tests/configuration_test.py | 103 python-certbot-0.28.0/certbot/tests/crypto_util_test.py | 323 +- python-certbot-0.28.0/certbot/tests/display/completer_test.py | 32 python-certbot-0.28.0/certbot/tests/display/ops_test.py | 164 + python-certbot-0.28.0/certbot/tests/display/util_test.py | 90 python-certbot-0.28.0/certbot/tests/eff_test.py | 155 + python-certbot-0.28.0/certbot/tests/error_handler_test.py | 48 python-certbot-0.28.0/certbot/tests/errors_test.py | 13 python-certbot-0.28.0/certbot/tests/hook_test.py | 573 +++- python-certbot-0.28.0/certbot/tests/lock_test.py | 118 python-certbot-0.28.0/certbot/tests/log_test.py | 421 +++ python-certbot-0.28.0/certbot/tests/main_test.py | 1062 ++++++-- python-certbot-0.28.0/certbot/tests/ocsp_test.py | 3 python-certbot-0.28.0/certbot/tests/renewal_test.py | 30 python-certbot-0.28.0/certbot/tests/renewupdater_test.py | 125 python-certbot-0.28.0/certbot/tests/reporter_test.py | 30 python-certbot-0.28.0/certbot/tests/reverter_test.py | 37 python-certbot-0.28.0/certbot/tests/storage_test.py | 325 +- python-certbot-0.28.0/certbot/tests/testdata/README | 11 python-certbot-0.28.0/certbot/tests/testdata/cert-5sans.pem | 16 python-certbot-0.28.0/certbot/tests/testdata/cert-5sans_512.pem | 16 python-certbot-0.28.0/certbot/tests/testdata/cert-nosans_nistp256.pem | 11 python-certbot-0.28.0/certbot/tests/testdata/cert-san.pem | 14 python-certbot-0.28.0/certbot/tests/testdata/cert-san_512.pem | 14 python-certbot-0.28.0/certbot/tests/testdata/cert.b64jose | 1 python-certbot-0.28.0/certbot/tests/testdata/cert.pem | 13 python-certbot-0.28.0/certbot/tests/testdata/cert_2048.pem | 20 python-certbot-0.28.0/certbot/tests/testdata/cert_512.pem | 13 python-certbot-0.28.0/certbot/tests/testdata/cert_512_bad.pem | 15 python-certbot-0.28.0/certbot/tests/testdata/cert_fullchain_2048.pem | 40 python-certbot-0.28.0/certbot/tests/testdata/csr-6sans.pem | 12 python-certbot-0.28.0/certbot/tests/testdata/csr-6sans_512.conf | 29 python-certbot-0.28.0/certbot/tests/testdata/csr-6sans_512.pem | 12 python-certbot-0.28.0/certbot/tests/testdata/csr-nonames.pem | 8 python-certbot-0.28.0/certbot/tests/testdata/csr-nonames_512.pem | 8 python-certbot-0.28.0/certbot/tests/testdata/csr-nosans.pem | 8 python-certbot-0.28.0/certbot/tests/testdata/csr-nosans_512.conf | 16 python-certbot-0.28.0/certbot/tests/testdata/csr-nosans_512.pem | 9 python-certbot-0.28.0/certbot/tests/testdata/csr-nosans_nistp256.pem | 8 python-certbot-0.28.0/certbot/tests/testdata/csr-san.pem | 10 python-certbot-0.28.0/certbot/tests/testdata/csr-san_512.pem | 10 python-certbot-0.28.0/certbot/tests/testdata/csr.pem | 10 python-certbot-0.28.0/certbot/tests/testdata/csr_512.pem | 8 python-certbot-0.28.0/certbot/tests/testdata/dsa512_key.pem | 14 python-certbot-0.28.0/certbot/tests/testdata/dsa_cert.pem | 17 python-certbot-0.28.0/certbot/tests/testdata/matching_cert.pem | 14 python-certbot-0.28.0/certbot/tests/testdata/nistp256_key.pem | 5 python-certbot-0.28.0/certbot/tests/testdata/rsa2048_key.pem | 28 python-certbot-0.28.0/certbot/tests/testdata/rsa512_key_2.pem | 9 python-certbot-0.28.0/certbot/tests/testdata/sample-renewal.conf | 2 python-certbot-0.28.0/certbot/tests/util.py | 248 + python-certbot-0.28.0/certbot/tests/util_test.py | 226 + python-certbot-0.28.0/certbot/updater.py | 122 python-certbot-0.28.0/certbot/util.py | 223 + python-certbot-0.28.0/debian/certbot.cron.d | 8 python-certbot-0.28.0/debian/certbot.dirs | 1 python-certbot-0.28.0/debian/certbot.logrotate | 6 python-certbot-0.28.0/debian/certbot.timer | 2 python-certbot-0.28.0/debian/changelog | 176 + python-certbot-0.28.0/debian/cli.ini | 3 python-certbot-0.28.0/debian/compat | 2 python-certbot-0.28.0/debian/control | 76 python-certbot-0.28.0/debian/control.in | 124 python-certbot-0.28.0/debian/copyright | 2 python-certbot-0.28.0/debian/patches/0001-remove-external-images.patch | 22 python-certbot-0.28.0/debian/patches/f5aad1440f8143f003698670177fabfc5fa7bb9c.patch | 74 python-certbot-0.28.0/debian/patches/series | 1 python-certbot-0.28.0/debian/python3-certbot.lintian-overrides | 3 python-certbot-0.28.0/debian/rules | 24 python-certbot-0.28.0/docs/_templates/footer.html | 52 python-certbot-0.28.0/docs/api/cert_manager.rst | 5 python-certbot-0.28.0/docs/api/cli.rst | 5 python-certbot-0.28.0/docs/api/constants.rst | 4 python-certbot-0.28.0/docs/api/eff.rst | 5 python-certbot-0.28.0/docs/api/error_handler.rst | 5 python-certbot-0.28.0/docs/api/hooks.rst | 5 python-certbot-0.28.0/docs/api/lock.rst | 5 python-certbot-0.28.0/docs/api/log.rst | 5 python-certbot-0.28.0/docs/api/main.rst | 5 python-certbot-0.28.0/docs/api/notify.rst | 5 python-certbot-0.28.0/docs/api/ocsp.rst | 5 python-certbot-0.28.0/docs/api/plugins/dns_common.rst | 5 python-certbot-0.28.0/docs/api/plugins/dns_common_lexicon.rst | 5 python-certbot-0.28.0/docs/api/plugins/selection.rst | 5 python-certbot-0.28.0/docs/api/renewal.rst | 5 python-certbot-0.28.0/docs/challenges.rst | 85 python-certbot-0.28.0/docs/ciphers.rst | 4 python-certbot-0.28.0/docs/cli-help.txt | 532 +++- python-certbot-0.28.0/docs/conf.py | 5 python-certbot-0.28.0/docs/contributing.rst | 404 +-- python-certbot-0.28.0/docs/index.rst | 1 python-certbot-0.28.0/docs/install.rst | 148 - python-certbot-0.28.0/docs/intro.rst | 3 python-certbot-0.28.0/docs/packaging.rst | 76 python-certbot-0.28.0/docs/using.rst | 677 ++++- python-certbot-0.28.0/docs/what.rst | 31 python-certbot-0.28.0/examples/cli.ini | 12 python-certbot-0.28.0/examples/dev-cli.ini | 2 python-certbot-0.28.0/setup.cfg | 9 python-certbot-0.28.0/setup.py | 59 175 files changed, 14932 insertions(+), 4433 deletions(-) diff -Nru python-certbot-0.10.2/CHANGELOG.md python-certbot-0.28.0/CHANGELOG.md --- python-certbot-0.10.2/CHANGELOG.md 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/CHANGELOG.md 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,1284 @@ +# Certbot change log + +Certbot adheres to [Semantic Versioning](http://semver.org/). + +## 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 propogate 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 Infrastracture 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 diff -Nru python-certbot-0.10.2/CHANGES.rst python-certbot-0.28.0/CHANGES.rst --- python-certbot-0.10.2/CHANGES.rst 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/CHANGES.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -ChangeLog -========= - -To see the changes in a given release, view the issues closed in a given -release's GitHub milestone: - - - `Past releases `_ - - `Upcoming releases `_ diff -Nru python-certbot-0.10.2/MANIFEST.in python-certbot-0.28.0/MANIFEST.in --- python-certbot-0.10.2/MANIFEST.in 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/MANIFEST.in 2018-11-07 21:14:56.000000000 +0000 @@ -1,8 +1,9 @@ include README.rst -include CHANGES.rst +include CHANGELOG.md include CONTRIBUTING.md include LICENSE.txt include linter_plugin.py recursive-include docs * recursive-include examples * recursive-include certbot/tests/testdata * +include certbot/ssl-dhparams.pem diff -Nru python-certbot-0.10.2/PKG-INFO python-certbot-0.28.0/PKG-INFO --- python-certbot-0.10.2/PKG-INFO 2017-01-26 02:58:41.000000000 +0000 +++ python-certbot-0.28.0/PKG-INFO 2018-11-07 21:14:58.000000000 +0000 @@ -1,6 +1,6 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: certbot -Version: 0.10.2 +Version: 0.28.0 Summary: ACME client Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -8,13 +8,13 @@ License: Apache License 2.0 Description: .. 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 - 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 identify 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. + 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, so there’s no need to arrange payment. - How you use Certbot depends on the configuration of your web server. 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. + How you use Certbot depends on the configuration of your web server. 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. - 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 issues by Let’s Encrypt. + Certbot is meant to be run directly on your web server, 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. Certbot is a fully-featured, extensible client for the Let's Encrypt CA (or any other CA that speaks the `ACME @@ -23,6 +23,9 @@ configuring webservers to use them. This client runs on Unix-based operating systems. + To see the changes made to Certbot between versions please refer to our + `changelog `_. + Until May 2016, Certbot was named simply ``letsencrypt`` or ``letsencrypt-auto``, depending on install method. Instructions on the Internet, and some pieces of the software, may still refer to this older name. @@ -96,32 +99,20 @@ Let's Encrypt Website: https://letsencrypt.org - IRC Channel: #letsencrypt on `Freenode`_ or #certbot on `OFTC`_ - Community: https://community.letsencrypt.org ACME spec: http://ietf-wg-acme.github.io/acme/ ACME working area in github: https://github.com/ietf-wg-acme/acme - - Mailing list: `client-dev`_ (to subscribe without a Google account, send an - email to client-dev+subscribe@letsencrypt.org) - |build-status| |coverage| |docs| |container| - .. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt - - .. _OFTC: https://webchat.oftc.net?channels=%23certbot - - .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev - .. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master :target: https://travis-ci.org/certbot/certbot :alt: Travis CI status - .. |coverage| image:: https://coveralls.io/repos/certbot/certbot/badge.svg?branch=master - :target: https://coveralls.io/r/certbot/certbot + .. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg + :target: https://codecov.io/gh/certbot/certbot :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ @@ -137,19 +128,7 @@ System Requirements =================== - The Let's Encrypt Client presently only runs on Unix-ish OSes that include - Python 2.6 or 2.7; Python 3.x support will hopefully be added in the future. The - client requires root access in order to write to ``/etc/letsencrypt``, - ``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to bind to ports 80 and 443 - (if you use the ``standalone`` plugin) and to read and modify webserver - configurations (if you use the ``apache`` or ``nginx`` plugins). If none of - these apply to you, it is theoretically possible to run without root privileges, - but for most users who want to avoid running an ACME client as root, either - `letsencrypt-nosudo `_ or - `simp_le `_ are more appropriate choices. - - The Apache plugin currently requires a Debian-based OS with augeas version - 1.0; this includes Ubuntu 12.04+ and Debian 7+. + See https://certbot.eff.org/docs/install.html#system-requirements. .. Do not modify this comment unless you know what you're doing. tag:intro-end @@ -160,8 +139,8 @@ * Supports multiple web servers: - - apache/2.x (beta support for auto-configuration) - - nginx/0.8.48+ (alpha support for auto-configuration) + - apache/2.x + - nginx/0.8.48+ - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - standalone (runs its own simple webserver to prove you control a domain) @@ -177,7 +156,7 @@ runs https only (Apache only) * Fully automated. * Configuration changes are logged and can be reverted. - * Supports ncurses and text (-t) UI, or can be driven entirely from the + * Supports an interactive text UI, or can be driven entirely from the command line. * Free and Open Source Software, made with Python. @@ -186,7 +165,7 @@ For extensive documentation on using and contributing to Certbot, go to https://certbot.eff.org/docs. If you would like to contribute to the project or run the latest code from git, you should read our `developer guide `_. Platform: UNKNOWN -Classifier: Development Status :: 3 - Alpha +Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: System Administrators @@ -194,11 +173,19 @@ Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 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: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Provides-Extra: dev3 +Provides-Extra: docs +Provides-Extra: dev diff -Nru python-certbot-0.10.2/README.rst python-certbot-0.28.0/README.rst --- python-certbot-0.10.2/README.rst 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/README.rst 2018-11-07 21:14:56.000000000 +0000 @@ -1,12 +1,12 @@ .. 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 -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 identify 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. +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, so there’s no need to arrange payment. -How you use Certbot depends on the configuration of your web server. 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. +How you use Certbot depends on the configuration of your web server. 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. -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 issues by Let’s Encrypt. +Certbot is meant to be run directly on your web server, 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. Certbot is a fully-featured, extensible client for the Let's Encrypt CA (or any other CA that speaks the `ACME @@ -15,6 +15,9 @@ configuring webservers to use them. This client runs on Unix-based operating systems. +To see the changes made to Certbot between versions please refer to our +`changelog `_. + Until May 2016, Certbot was named simply ``letsencrypt`` or ``letsencrypt-auto``, depending on install method. Instructions on the Internet, and some pieces of the software, may still refer to this older name. @@ -88,32 +91,20 @@ Let's Encrypt Website: https://letsencrypt.org -IRC Channel: #letsencrypt on `Freenode`_ or #certbot on `OFTC`_ - Community: https://community.letsencrypt.org ACME spec: http://ietf-wg-acme.github.io/acme/ ACME working area in github: https://github.com/ietf-wg-acme/acme - -Mailing list: `client-dev`_ (to subscribe without a Google account, send an -email to client-dev+subscribe@letsencrypt.org) - |build-status| |coverage| |docs| |container| -.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt - -.. _OFTC: https://webchat.oftc.net?channels=%23certbot - -.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev - .. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master :target: https://travis-ci.org/certbot/certbot :alt: Travis CI status -.. |coverage| image:: https://coveralls.io/repos/certbot/certbot/badge.svg?branch=master - :target: https://coveralls.io/r/certbot/certbot +.. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg + :target: https://codecov.io/gh/certbot/certbot :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ @@ -129,19 +120,7 @@ System Requirements =================== -The Let's Encrypt Client presently only runs on Unix-ish OSes that include -Python 2.6 or 2.7; Python 3.x support will hopefully be added in the future. The -client requires root access in order to write to ``/etc/letsencrypt``, -``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to bind to ports 80 and 443 -(if you use the ``standalone`` plugin) and to read and modify webserver -configurations (if you use the ``apache`` or ``nginx`` plugins). If none of -these apply to you, it is theoretically possible to run without root privileges, -but for most users who want to avoid running an ACME client as root, either -`letsencrypt-nosudo `_ or -`simp_le `_ are more appropriate choices. - -The Apache plugin currently requires a Debian-based OS with augeas version -1.0; this includes Ubuntu 12.04+ and Debian 7+. +See https://certbot.eff.org/docs/install.html#system-requirements. .. Do not modify this comment unless you know what you're doing. tag:intro-end @@ -152,8 +131,8 @@ * Supports multiple web servers: - - apache/2.x (beta support for auto-configuration) - - nginx/0.8.48+ (alpha support for auto-configuration) + - apache/2.x + - nginx/0.8.48+ - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - standalone (runs its own simple webserver to prove you control a domain) @@ -169,7 +148,7 @@ runs https only (Apache only) * Fully automated. * Configuration changes are logged and can be reverted. -* Supports ncurses and text (-t) UI, or can be driven entirely from the +* Supports an interactive text UI, or can be driven entirely from the command line. * Free and Open Source Software, made with Python. diff -Nru python-certbot-0.10.2/certbot/__init__.py python-certbot-0.28.0/certbot/__init__.py --- python-certbot-0.10.2/certbot/__init__.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/__init__.py 2018-11-07 21:14:57.000000000 +0000 @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.10.2' +__version__ = '0.28.0' diff -Nru python-certbot-0.10.2/certbot/account.py python-certbot-0.28.0/certbot/account.py --- python-certbot-0.10.2/certbot/account.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/account.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,20 +1,24 @@ """Creates ACME accounts for server.""" import datetime +import functools import hashlib import logging import os +import shutil import socket from cryptography.hazmat.primitives import serialization +import josepy as jose import pyrfc3339 import pytz import six import zope.component from acme import fields as acme_fields -from acme import jose from acme import messages +from certbot import compat +from certbot import constants from certbot import errors from certbot import interfaces from certbot import util @@ -73,7 +77,8 @@ self.meta.creation_dt), self.meta.creation_host, self.id[:4]) def __repr__(self): - return "<{0}({1})>".format(self.__class__.__name__, self.id) + return "<{0}({1}, {2}, {3})>".format( + self.__class__.__name__, self.regr, self.id, self.meta) def __eq__(self, other): return (isinstance(other, self.__class__) and @@ -81,7 +86,7 @@ self.meta == other.meta) -def report_new_account(acc, config): +def report_new_account(config): """Informs the user about their new ACME account.""" reporter = zope.component.queryUtility(interfaces.IReporter) if reporter is None: @@ -95,15 +100,9 @@ config.config_dir), reporter.MEDIUM_PRIORITY) - if acc.regr.body.emails: - recovery_msg = ("If you lose your account credentials, you can " - "recover through e-mails sent to {0}.".format( - ", ".join(acc.regr.body.emails))) - reporter.add_message(recovery_msg, reporter.MEDIUM_PRIORITY) - class AccountMemoryStorage(interfaces.AccountStorage): - """In-memory account strage.""" + """In-memory account storage.""" def __init__(self, initial_accounts=None): self.accounts = initial_accounts if initial_accounts is not None else {} @@ -111,7 +110,8 @@ def find_all(self): return list(six.itervalues(self.accounts)) - def save(self, account): + def save(self, account, acme): + # pylint: disable=unused-argument if account.id in self.accounts: logger.debug("Overwriting account: %s", account.id) self.accounts[account.id] = account @@ -122,6 +122,16 @@ except KeyError: raise errors.AccountNotFound(account_id) +class RegistrationResourceWithNewAuthzrURI(messages.RegistrationResource): + """A backwards-compatible RegistrationResource with a new-authz URI. + + Hack: Certbot versions pre-0.11.1 expect to load + new_authzr_uri as part of the account. Because people + sometimes switch between old and new versions, we will + continue to write out this field for some time so older + clients don't crash in that scenario. + """ + new_authzr_uri = jose.Field('new_authzr_uri') class AccountFileStorage(interfaces.AccountStorage): """Accounts file storage. @@ -131,11 +141,15 @@ """ def __init__(self, config): self.config = config - util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid(), + util.make_or_verify_dir(config.accounts_dir, 0o700, compat.os_geteuid(), self.config.strict_permissions) def _account_dir_path(self, account_id): - return os.path.join(self.config.accounts_dir, account_id) + return self._account_dir_path_for_server_path(account_id, self.config.server_path) + + def _account_dir_path_for_server_path(self, account_id, server_path): + 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): @@ -149,25 +163,67 @@ def _metadata_path(cls, account_dir_path): return os.path.join(account_dir_path, "meta.json") - def find_all(self): + def _find_all_for_server_path(self, server_path): + accounts_dir = self.config.accounts_dir_for_server_path(server_path) try: - candidates = os.listdir(self.config.accounts_dir) + candidates = os.listdir(accounts_dir) except OSError: return [] accounts = [] for account_id in candidates: try: - accounts.append(self.load(account_id)) + 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 load(self, account_id): - account_dir_path = self._account_dir_path(account_id) - if not os.path.isdir(account_dir_path): - raise errors.AccountNotFound( - "Account at %s does not exist" % account_dir_path) + def find_all(self): + return self._find_all_for_server_path(self.config.server_path) + + def _symlink_to_account_dir(self, prev_server_path, server_path, account_id): + 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, server_path): + 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, server_path): + 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 + else: + raise errors.AccountNotFound( + "Account at %s does not exist" % account_dir_path) try: with open(self._regr_path(account_dir_path)) as regr_file: @@ -186,24 +242,107 @@ account_id, acc.id)) return acc - def save(self, account): - self._save(account, regr_only=False) + def load(self, account_id): + return self._load_for_server_path(account_id, self.config.server_path) + + def save(self, account, acme): + self._save(account, acme, regr_only=False) - def save_regr(self, account): + def save_regr(self, account, acme): """Save the registration resource. :param Account account: account whose regr should be saved """ - self._save(account, regr_only=True) + self._save(account, acme, regr_only=True) + + def delete(self, account_id): + """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( + "Account at %s does not exist" % account_dir_path) + # 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, server_path): + 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): + 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, link_func): + """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 in constants.LE_REUSE_SERVERS: + reused_servers[constants.LE_REUSE_SERVERS[k]] = 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 os.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 = os.readlink(dir_path) + os.unlink(dir_path) + dir_path = target + + return dir_path - def _save(self, account, regr_only): + def _save(self, account, acme, regr_only): account_dir_path = self._account_dir_path(account.id) - util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), + util.make_or_verify_dir(account_dir_path, 0o700, compat.os_geteuid(), self.config.strict_permissions) try: with open(self._regr_path(account_dir_path), "w") as regr_file: - regr_file.write(account.regr.json_dumps()) + regr = account.regr + # If we have a value for new-authz, save it for forwards + # compatibility with older versions of Certbot. If we don't + # have a value for new-authz, this is an ACMEv2 directory where + # an older version of Certbot won't work anyway. + if hasattr(acme.directory, "new-authz"): + regr = RegistrationResourceWithNewAuthzrURI( + new_authzr_uri=acme.directory.new_authz, + body={}, + uri=regr.uri) + else: + regr = messages.RegistrationResource( + body={}, + uri=regr.uri) + regr_file.write(regr.json_dumps()) if not regr_only: with util.safe_open(self._key_path(account_dir_path), "w", chmod=0o400) as key_file: diff -Nru python-certbot-0.10.2/certbot/achallenges.py python-certbot-0.28.0/certbot/achallenges.py --- python-certbot-0.10.2/certbot/achallenges.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/achallenges.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,6 +1,6 @@ """Client annotated ACME challenges. -Please use names such as ``achall`` to distiguish from variables "of type" +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``):: @@ -19,8 +19,9 @@ """ import logging +import josepy as jose + from acme import challenges -from acme import jose logger = logging.getLogger(__name__) @@ -28,7 +29,6 @@ # pylint: disable=too-few-public-methods - class AnnotatedChallenge(jose.ImmutableMap): """Client annotated challenge. diff -Nru python-certbot-0.10.2/certbot/auth_handler.py python-certbot-0.28.0/certbot/auth_handler.py --- python-certbot-0.10.2/certbot/auth_handler.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/auth_handler.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,4 +1,5 @@ """ACME AuthHandler.""" +import collections import logging import time @@ -7,7 +8,9 @@ from acme import challenges from acme import messages - +# pylint: disable=unused-import, no-name-in-module +from acme.magic_typing import DefaultDict, Dict, List, Set, Collection +# pylint: enable=unused-import, no-name-in-module from certbot import achallenges from certbot import errors from certbot import error_handler @@ -17,6 +20,10 @@ logger = logging.getLogger(__name__) +AnnotatedAuthzr = collections.namedtuple("AnnotatedAuthzr", ["authzr", "achalls"]) +"""Stores an authorization resource and its active annotated challenges.""" + + class AuthHandler(object): """ACME Authorization Handler for a client. @@ -24,15 +31,11 @@ :class:`~acme.challenges.Challenge` types :type auth: :class:`certbot.interfaces.IAuthenticator` - :ivar acme.client.Client acme: ACME client API. + :ivar acme.client.BackwardsCompatibleClientV2 acme: ACME client API. :ivar account: Client's Account :type account: :class:`certbot.account.Account` - :ivar dict authzr: ACME Authorization Resource dict where keys are domains - and values are :class:`acme.messages.AuthorizationResource` - :ivar list achalls: DV challenges in the form of - :class:`certbot.achallenges.AnnotatedChallenge` :ivar list pref_challs: sorted user specified preferred challenges type strings with the most preferred challenge listed first @@ -42,18 +45,15 @@ self.acme = acme self.account = account - self.authzr = dict() self.pref_challs = pref_challs - # List must be used to keep responses straight. - self.achalls = [] - - def get_authorizations(self, domains, best_effort=False): + def handle_authorizations(self, orderr, best_effort=False): """Retrieve all authorizations for challenges. - :param list domains: Domains for authorization + :param acme.messages.OrderResource orderr: must have + authorizations filled in :param bool best_effort: Whether or not all authorizations are - required (this is useful in renewal) + required (this is useful in renewal) :returns: List of authorization resources :rtype: list @@ -62,26 +62,31 @@ authorizations """ - for domain in domains: - self.authzr[domain] = self.acme.request_domain_challenges( - domain, self.account.regr.new_authzr_uri) + aauthzrs = [AnnotatedAuthzr(authzr, []) + for authzr in orderr.authorizations] - self._choose_challenges(domains) + self._choose_challenges(aauthzrs) + config = zope.component.getUtility(interfaces.IConfig) + notify = zope.component.getUtility(interfaces.IDisplay).notification # While there are still challenges remaining... - while self.achalls: - resp = self._solve_challenges() - logger.info("Waiting for verification...") + while self._has_challenges(aauthzrs): + with error_handler.ExitHandler(self._cleanup_challenges, aauthzrs): + resp = self._solve_challenges(aauthzrs) + logger.info("Waiting for verification...") + if config.debug_challenges: + notify('Challenges loaded. Press continue to submit to CA. ' + 'Pass "-v" for more info about challenges.', pause=True) - # Send all Responses - this modifies achalls - self._respond(resp, best_effort) + # Send all Responses - this modifies achalls + self._respond(aauthzrs, resp, best_effort) # Just make sure all decisions are complete. - self.verify_authzr_complete() + self.verify_authzr_complete(aauthzrs) # Only return valid authorizations - retVal = [authzr for authzr in self.authzr.values() - if authzr.body.status == messages.STATUS_VALID] + retVal = [aauthzr.authzr for aauthzr in aauthzrs + if aauthzr.authzr.body.status == messages.STATUS_VALID] if not retVal: raise errors.AuthorizationError( @@ -89,106 +94,135 @@ return retVal - def _choose_challenges(self, domains): + def _choose_challenges(self, aauthzrs): """Retrieve necessary challenges to satisfy server.""" logger.info("Performing the following challenges:") - for dom in domains: + for aauthzr in aauthzrs: + aauthzr_challenges = aauthzr.authzr.body.challenges + if self.acme.acme_version == 1: + combinations = aauthzr.authzr.body.combinations + else: + combinations = tuple((i,) for i in range(len(aauthzr_challenges))) + path = gen_challenge_path( - self.authzr[dom].body.challenges, - self._get_chall_pref(dom), - self.authzr[dom].body.combinations) - - dom_achalls = self._challenge_factory( - dom, path) - self.achalls.extend(dom_achalls) + aauthzr_challenges, + self._get_chall_pref(aauthzr.authzr.body.identifier.value), + combinations) + + aauthzr_achalls = self._challenge_factory( + aauthzr.authzr, path) + aauthzr.achalls.extend(aauthzr_achalls) + + for aauthzr in aauthzrs: + for achall in aauthzr.achalls: + if isinstance(achall.chall, challenges.TLSSNI01): + logger.warning("TLS-SNI-01 is deprecated, and will stop working soon.") + return + + def _has_challenges(self, aauthzrs): + """Do we have any challenges to perform?""" + return any(aauthzr.achalls for aauthzr in aauthzrs) - def _solve_challenges(self): + def _solve_challenges(self, aauthzrs): """Get Responses for challenges from authenticators.""" - resp = [] - with error_handler.ErrorHandler(self._cleanup_challenges): - try: - if self.achalls: - resp = self.auth.perform(self.achalls) - except errors.AuthorizationError: - logger.critical("Failure in setting up challenges.") - logger.info("Attempting to clean up outstanding challenges...") - raise + resp = [] # type: Collection[acme.challenges.ChallengeResponse] + all_achalls = self._get_all_achalls(aauthzrs) + try: + if all_achalls: + resp = self.auth.perform(all_achalls) + except errors.AuthorizationError: + logger.critical("Failure in setting up challenges.") + logger.info("Attempting to clean up outstanding challenges...") + raise - assert len(resp) == len(self.achalls) + assert len(resp) == len(all_achalls) return resp - def _respond(self, resp, best_effort): + def _get_all_achalls(self, aauthzrs): + """Return all active challenges.""" + all_achalls = [] # type: Collection[challenges.ChallengeResponse] + for aauthzr in aauthzrs: + all_achalls.extend(aauthzr.achalls) + return all_achalls + + def _respond(self, aauthzrs, resp, best_effort): """Send/Receive confirmation of all challenges. .. note:: This method also cleans up the auth_handler state. """ # TODO: chall_update is a dirty hack to get around acme-spec #105 - chall_update = dict() - active_achalls = self._send_responses(self.achalls, - resp, chall_update) + chall_update = dict() \ + # type: Dict[int, List[achallenges.KeyAuthorizationAnnotatedChallenge]] + self._send_responses(aauthzrs, resp, chall_update) # Check for updated status... - try: - self._poll_challenges(chall_update, best_effort) - finally: - # This removes challenges from self.achalls - self._cleanup_challenges(active_achalls) + self._poll_challenges(aauthzrs, chall_update, best_effort) - def _send_responses(self, achalls, resps, chall_update): + def _send_responses(self, aauthzrs, resps, chall_update): """Send responses and make sure errors are handled. + :param aauthzrs: authorizations and the selected annotated challenges + to try and perform + :type aauthzrs: `list` of `AnnotatedAuthzr` + :param resps: challenge responses from the authenticator where + each response at index i corresponds to the annotated + challenge at index i in the list returned by + :func:`_get_all_achalls` + :type resps: `collections.abc.Iterable` of + :class:`~acme.challenges.ChallengeResponse` or `False` or + `None` :param dict chall_update: parameter that is updated to hold - authzr -> list of outstanding solved annotated challenges + aauthzr index to list of outstanding solved annotated challenges """ active_achalls = [] - for achall, resp in six.moves.zip(achalls, resps): - # This line needs to be outside of the if block below to - # ensure failed challenges are cleaned up correctly - active_achalls.append(achall) - - # Don't send challenges for None and False authenticator responses - if resp is not None and resp: - self.acme.answer_challenge(achall.challb, resp) - # TODO: answer_challenge returns challr, with URI, - # that can be used in _find_updated_challr - # comparisons... - if achall.domain in chall_update: - chall_update[achall.domain].append(achall) - else: - chall_update[achall.domain] = [achall] + resps_iter = iter(resps) + for i, aauthzr in enumerate(aauthzrs): + for achall in aauthzr.achalls: + # This line needs to be outside of the if block below to + # ensure failed challenges are cleaned up correctly + active_achalls.append(achall) + + resp = next(resps_iter) + # Don't send challenges for None and False authenticator responses + if resp: + self.acme.answer_challenge(achall.challb, resp) + # TODO: answer_challenge returns challr, with URI, + # that can be used in _find_updated_challr + # comparisons... + chall_update.setdefault(i, []).append(achall) return active_achalls - def _poll_challenges( - self, chall_update, best_effort, min_sleep=3, max_rounds=15): + def _poll_challenges(self, aauthzrs, chall_update, + best_effort, min_sleep=3, max_rounds=30): """Wait for all challenge results to be determined.""" - dom_to_check = set(chall_update.keys()) - comp_domains = set() + indices_to_check = set(chall_update.keys()) + comp_indices = set() rounds = 0 - while dom_to_check and rounds < max_rounds: + while indices_to_check and rounds < max_rounds: # TODO: Use retry-after... time.sleep(min_sleep) - all_failed_achalls = set() - for domain in dom_to_check: + all_failed_achalls = set() # type: Set[achallenges.KeyAuthorizationAnnotatedChallenge] + for index in indices_to_check: comp_achalls, failed_achalls = self._handle_check( - domain, chall_update[domain]) + aauthzrs, index, chall_update[index]) - if len(comp_achalls) == len(chall_update[domain]): - comp_domains.add(domain) + if len(comp_achalls) == len(chall_update[index]): + comp_indices.add(index) elif not failed_achalls: for achall, _ in comp_achalls: - chall_update[domain].remove(achall) + chall_update[index].remove(achall) # We failed some challenges... damage control else: if best_effort: - comp_domains.add(domain) + comp_indices.add(index) logger.warning( "Challenge failed for domain %s", - domain) + aauthzrs[index].authzr.body.identifier.value) else: all_failed_achalls.update( updated for _, updated in failed_achalls) @@ -197,24 +231,26 @@ _report_failed_challs(all_failed_achalls) raise errors.FailedChallenges(all_failed_achalls) - dom_to_check -= comp_domains - comp_domains.clear() + indices_to_check -= comp_indices + comp_indices.clear() rounds += 1 - def _handle_check(self, domain, achalls): + def _handle_check(self, aauthzrs, index, achalls): """Returns tuple of ('completed', 'failed').""" completed = [] failed = [] - self.authzr[domain], _ = self.acme.poll(self.authzr[domain]) - if self.authzr[domain].body.status == messages.STATUS_VALID: + original_aauthzr = aauthzrs[index] + updated_authzr, _ = self.acme.poll(original_aauthzr.authzr) + aauthzrs[index] = AnnotatedAuthzr(updated_authzr, original_aauthzr.achalls) + if updated_authzr.body.status == messages.STATUS_VALID: return achalls, [] # Note: if the whole authorization is invalid, the individual failed # challenges will be determined here... for achall in achalls: updated_achall = achall.update(challb=self._find_updated_challb( - self.authzr[domain], achall)) + updated_authzr, achall)) # This does nothing for challenges that have yet to be decided yet. if updated_achall.status == messages.STATUS_VALID: @@ -263,40 +299,48 @@ chall_prefs.extend(plugin_pref) return chall_prefs - def _cleanup_challenges(self, achall_list=None): + def _cleanup_challenges(self, aauthzrs, achalls=None): """Cleanup challenges. - If achall_list is not provided, cleanup all achallenges. + :param aauthzrs: authorizations and their selected annotated + challenges + :type aauthzrs: `list` of `AnnotatedAuthzr` + :param achalls: annotated challenges to cleanup + :type achalls: `list` of :class:`certbot.achallenges.AnnotatedChallenge` """ logger.info("Cleaning up challenges") - - if achall_list is None: - achalls = self.achalls - else: - achalls = achall_list - + if achalls is None: + achalls = self._get_all_achalls(aauthzrs) if achalls: self.auth.cleanup(achalls) for achall in achalls: - self.achalls.remove(achall) + for aauthzr in aauthzrs: + if achall in aauthzr.achalls: + aauthzr.achalls.remove(achall) + break - def verify_authzr_complete(self): + def verify_authzr_complete(self, aauthzrs): """Verifies that all authorizations have been decided. + :param aauthzrs: authorizations and their selected annotated + challenges + :type aauthzrs: `list` of `AnnotatedAuthzr` + :returns: Whether all authzr are complete :rtype: bool """ - for authzr in self.authzr.values(): + for aauthzr in aauthzrs: + authzr = aauthzr.authzr if (authzr.body.status != messages.STATUS_VALID and authzr.body.status != messages.STATUS_INVALID): raise errors.AuthorizationError("Incomplete authorizations") - def _challenge_factory(self, domain, path): + def _challenge_factory(self, authzr, path): """Construct Namedtuple Challenges - :param str domain: domain of the enrollee + :param messages.AuthorizationResource authzr: authorization :param list path: List of indices from `challenges`. @@ -310,8 +354,9 @@ achalls = [] for index in path: - challb = self.authzr[domain].body.challenges[index] - achalls.append(challb_to_achall(challb, self.account.key, domain)) + challb = authzr.body.challenges[index] + achalls.append(challb_to_achall( + challb, self.account.key, authzr.body.identifier.value)) return achalls @@ -387,7 +432,7 @@ # max_cost is now equal to sum(indices) + 1 - best_combo = [] + best_combo = None # Set above completing all of the available challenges best_combo_cost = max_cost @@ -404,7 +449,7 @@ combo_total = 0 if not best_combo: - _report_no_chall_path() + _report_no_chall_path(challbs) return best_combo @@ -425,22 +470,30 @@ if supported: path.append(i) else: - _report_no_chall_path() + _report_no_chall_path(challbs) return path -def _report_no_chall_path(): - """Logs and raises an error that no satisfiable chall path exists.""" +def _report_no_chall_path(challbs): + """Logs and raises an error that no satisfiable chall path exists. + + :param challbs: challenges from the authorization that can't be satisfied + + """ msg = ("Client with the currently selected authenticator does not support " "any combination of challenges that will satisfy the CA.") - logger.fatal(msg) + 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) raise errors.AuthorizationError(msg) _ERROR_HELP_COMMON = ( "To fix these errors, please make sure that your domain name was entered " - "correctly and the DNS A record(s) for that domain contain(s) the " + "correctly and the DNS A/AAAA record(s) for that domain contain(s) the " "right IP address.") @@ -477,11 +530,11 @@ :class:`certbot.achallenges.AnnotatedChallenge`. """ - problems = dict() + problems = collections.defaultdict(list)\ + # type: DefaultDict[str, List[achallenges.KeyAuthorizationAnnotatedChallenge]] for achall in failed_achalls: if achall.error: - problems.setdefault(achall.error.typ, []).append(achall) - + problems[achall.error.typ].append(achall) reporter = zope.component.getUtility(interfaces.IReporter) for achalls in six.itervalues(problems): reporter.add_message( diff -Nru python-certbot-0.10.2/certbot/cert_manager.py python-certbot-0.28.0/certbot/cert_manager.py --- python-certbot-0.10.2/certbot/cert_manager.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/cert_manager.py 2018-11-07 21:14:56.000000000 +0000 @@ -3,9 +3,13 @@ import logging import os import pytz +import re import traceback import zope.component +from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module +from certbot import compat +from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot import ocsp @@ -44,7 +48,7 @@ """ disp = zope.component.getUtility(interfaces.IDisplay) - certname = _get_certname(config, "rename") + certname = get_certnames(config, "rename")[0] new_certname = config.new_certname if not new_certname: @@ -73,6 +77,7 @@ 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 " @@ -85,40 +90,61 @@ def delete(config): """Delete Certbot files associated with a certificate lineage.""" - certname = _get_certname(config, "delete") - storage.delete_files(config, certname) - disp = zope.component.getUtility(interfaces.IDisplay) - disp.notification("Deleted all files relating to certificate {0}." - .format(certname), pause=False) + certnames = get_certnames(config, "delete", allow_multiple=True) + for certname in certnames: + storage.delete_files(config, certname) + disp = zope.component.getUtility(interfaces.IDisplay) + disp.notification("Deleted all files relating to certificate {0}." + .format(certname), pause=False) ################### # Public Helpers ################### -def lineage_for_certname(config, certname): +def lineage_for_certname(cli_config, certname): """Find a lineage object with name certname.""" - def update_cert_for_name_match(candidate_lineage, rv): - """Return cert if it has name certname, else return rv - """ - matching_lineage_name_cert = rv - if candidate_lineage.lineagename == certname: - matching_lineage_name_cert = candidate_lineage - return matching_lineage_name_cert - return _search_lineages(config, update_cert_for_name_match, None) + configs_dir = cli_config.renewal_configs_dir + # Verify the directory is there + util.make_or_verify_dir(configs_dir, mode=0o755, uid=compat.os_geteuid()) + 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, certname): """Find the domains in the cert with name certname.""" - def update_domains_for_name_match(candidate_lineage, rv): - """Return domains if certname matches, else return rv - """ - matching_domains = rv - if candidate_lineage.lineagename == certname: - matching_domains = candidate_lineage.names() - return matching_domains - return _search_lineages(config, update_domains_for_name_match, None) + lineage = lineage_for_certname(config, certname) + return lineage.names() if lineage else None def find_duplicative_certs(config, domains): - """Find existing certs that duplicate the request.""" + """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.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, rv): """Return cert as identical_names_cert if it matches, or subset_names_cert if it matches as subset @@ -140,28 +166,168 @@ return _search_lineages(config, update_certs_for_domain_matches, (None, None)) +def _archive_files(candidate_lineage, filetype): + """ 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". -################### -# Private Helpers -################### + :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 len(pattern) > 0: + return pattern + else: + return None + +def _acceptable_matches(): + """ 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): + """ 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[0], lambda x: x.lineagename) + return match[0] + +def match_and_check_overlaps(cli_config, acceptable_matches, match_func, rv_func): + """ Searches through all lineages for a match, and checks for duplicates. + If a duplicate is found, an error is raised, as performing operations on lineages + that have their properties incorrectly duplicated elsewhere is probably a bad idea. + + :param `configuration.NamespaceConfig` cli_config: parsed command line arguments + :param list acceptable_matches: a list of functions that specify acceptable matches + :param function match_func: specifies what to match + :param function rv_func: specifies what to return + + """ + def find_matches(candidate_lineage, return_value, acceptable_matches): + """Returns a list of matches using _search_lineages.""" + acceptable_matches = [func(candidate_lineage) for func in acceptable_matches] + acceptable_matches_rv = [] # type: List[str] + for item in acceptable_matches: + if isinstance(item, list): + acceptable_matches_rv += item + else: + acceptable_matches_rv.append(item) + match = match_func(candidate_lineage) + if match in acceptable_matches_rv: + return_value.append(rv_func(candidate_lineage)) + return return_value + + matched = _search_lineages(cli_config, find_matches, [], acceptable_matches) + if not matched: + raise errors.Error("No match found for cert-path {0}!".format(cli_config.cert_path[0])) + elif len(matched) > 1: + raise errors.OverlappingMatchFound() + else: + return matched + +def human_readable_cert_info(config, cert, skip_filter_checks=False): + """ 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 "" + if config.domains and not set(config.domains).issubset(cert.names()): + return "" + now = pytz.UTC.fromutc(datetime.datetime.utcnow()) + + reasons = [] + if cert.is_test_cert: + reasons.append('TEST_CERT') + if cert.target_expiry <= now: + reasons.append('EXPIRED') + if checker.ocsp_revoked(cert.cert, cert.chain): + 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 = "VALID: {0} hour(s)".format(diff.seconds // 3600) + else: + status = "VALID: {0} days".format(diff.days) -def _get_certname(config, verb): + valid_string = "{0} ({1})".format(cert.target_expiry, status) + certinfo.append(" Certificate Name: {0}\n" + " Domains: {1}\n" + " Expiry Date: {2}\n" + " Certificate Path: {3}\n" + " Private Key Path: {4}".format( + cert.lineagename, + " ".join(cert.names()), + valid_string, + cert.fullchain, + cert.privkey)) + return "".join(certinfo) + +def get_certnames(config, verb, allow_multiple=False, custom_prompt=None): """Get certname from flag, interactively, or error out. """ certname = config.certname - if not certname: + if certname: + certnames = [certname] + else: disp = zope.component.getUtility(interfaces.IDisplay) 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.") - code, index = disp.menu("Which certificate would you like to {0}?".format(verb), - choices, ok_label="Select", flag="--cert-name", - force_interactive=True) - if code != display_util.OK or not index in range(0, len(choices)): - raise errors.Error("User ended interaction.") - certname = choices[index] - return certname + if allow_multiple: + if not custom_prompt: + prompt = "Which certificate(s) would you like to {0}?".format(verb) + else: + prompt = custom_prompt + code, certnames = disp.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 = disp.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): """Format a results report for a category of single-line renewal outcomes""" @@ -170,49 +336,13 @@ def _report_human_readable(config, parsed_certs): """Format a results report for a parsed cert""" certinfo = [] - checker = ocsp.RevocationChecker() for cert in parsed_certs: - if config.certname and cert.lineagename != config.certname: - continue - if config.domains and not set(config.domains).issubset(cert.names()): - continue - now = pytz.UTC.fromutc(datetime.datetime.utcnow()) - - reasons = [] - if cert.is_test_cert: - reasons.append('TEST_CERT') - if cert.target_expiry <= now: - reasons.append('EXPIRED') - if checker.ocsp_revoked(cert.cert, cert.chain): - 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 = "VALID: {0} hour(s)".format(diff.seconds // 3600) - else: - status = "VALID: {0} days".format(diff.days) - - valid_string = "{0} ({1})".format(cert.target_expiry, status) - certinfo.append(" Certificate Name: {0}\n" - " Domains: {1}\n" - " Expiry Date: {2}\n" - " Certificate Path: {3}\n" - " Private Key Path: {4}".format( - cert.lineagename, - " ".join(cert.names()), - valid_string, - cert.fullchain, - cert.privkey)) + certinfo.append(human_readable_cert_info(config, cert)) return "\n".join(certinfo) def _describe_certs(config, parsed_certs, parse_failures): """Print information about the certs we know about""" - out = [] + out = [] # type: List[str] notify = out.append @@ -224,22 +354,28 @@ notify("Found the following {0}certs:".format(match)) notify(_report_human_readable(config, parsed_certs)) if parse_failures: - notify("\nThe following renewal configuration files " + notify("\nThe following renewal configurations " "were invalid:") notify(_report_lines(parse_failures)) disp = zope.component.getUtility(interfaces.IDisplay) disp.notification("\n".join(out), pause=False, wrap=False) -def _search_lineages(cli_config, func, initial_rv): +def _search_lineages(cli_config, func, initial_rv, *args): """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, uid=os.geteuid()) + util.make_or_verify_dir(configs_dir, mode=0o755, uid=compat.os_geteuid()) rv = initial_rv for renewal_file in storage.renewal_conf_files(cli_config): @@ -249,5 +385,5 @@ 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) + rv = func(candidate_lineage, rv, *args) return rv diff -Nru python-certbot-0.10.2/certbot/cli.py python-certbot-0.28.0/certbot/cli.py --- python-certbot-0.10.2/certbot/cli.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/cli.py 2018-11-07 21:14:56.000000000 +0000 @@ -11,24 +11,34 @@ import configargparse import six +import zope.component +import zope.interface + +from zope.interface import interfaces as zope_interfaces from acme import challenges +# pylint: disable=unused-import, no-name-in-module +from acme.magic_typing import Any, Dict, Optional +# pylint: enable=unused-import, no-name-in-module import certbot from certbot import constants from certbot import crypto_util from certbot import errors +from certbot import hooks from certbot import interfaces from certbot import util +from certbot.display import util as display_util from certbot.plugins import disco as plugins_disco +import certbot.plugins.enhancements as enhancements import certbot.plugins.selection as plugin_selection logger = logging.getLogger(__name__) # Global, to save us from a lot of argument passing within the scope of this module -helpful_parser = None +helpful_parser = None # type: Optional[HelpfulArgumentParser] # For help strings, figure out how the user ran us. # When invoked from letsencrypt-auto, sys.argv[0] is something like: @@ -44,8 +54,13 @@ # user saved the script under a different name LEAUTO = os.path.basename(os.environ["CERTBOT_AUTO"]) -fragment = os.path.join(".local", "share", "letsencrypt") -cli_command = LEAUTO if fragment in sys.argv[0] else "certbot" +old_path_fragment = os.path.join(".local", "share", "letsencrypt") +new_path_prefix = os.path.abspath(os.path.join(os.sep, "opt", + "eff.org", "certbot", "venv")) +if old_path_fragment in sys.argv[0] or sys.argv[0].startswith(new_path_prefix): + cli_command = LEAUTO +else: + 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... @@ -56,31 +71,32 @@ Certbot can obtain and install HTTPS/TLS/SSL certificates. By default, it will attempt to use a webserver both for obtaining and installing the -cert. """.format(cli_command) +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 cert in your current webserver - certonly Obtain or renew a cert, but do not install it - renew Renew all previously obtained certs that are near expiry - -d DOMAINS Comma-separated list of domains to obtain a cert for + (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 certs interactively, or using shell script hooks + --manual Obtain certificates interactively, or using shell script hooks -n Run non-interactively - --test-cert Obtain a test cert from a staging server - --dry-run Test "renew" or "certonly" without saving any certs to disk + --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 certs you have from Certbot - revoke Revoke a certificate (supply --cert-path) + certificates Display information about certificates you have from Certbot + revoke Revoke a certificate (supply --cert-path or --cert-name) delete Delete a certificate manage your account with Let's Encrypt: @@ -119,6 +135,7 @@ # 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": set(("server",)), + "renew_hook": set(("deploy_hook",)), "server": set(("dry_run", "staging",)), "webroot_map": set(("webroot_path",))} @@ -131,14 +148,14 @@ between config options. :param modified: config options that can be modified by modifiers - :type modified: iterable or str + :type modified: iterable or str (string_types) :param modifiers: config options that modify modified - :type modifiers: iterable or str + :type modifiers: iterable or str (string_types) """ - if isinstance(modified, str): + if isinstance(modified, six.string_types): modified = (modified,) - if isinstance(modifiers, str): + if isinstance(modifiers, six.string_types): modifiers = (modifiers,) for var in modified: @@ -150,7 +167,7 @@ if cli_command != LEAUTO: return if config.no_self_upgrade: - # users setting --no-self-upgrade might be hanging on a clent version like 0.3.0 + # users setting --no-self-upgrade might be hanging on a client version like 0.3.0 # or 0.5.0 which is the new script, but doesn't set CERTBOT_AUTO; they don't # need warnings return @@ -184,30 +201,35 @@ (CLI or config file) including if the user explicitly set it to the default. Returns False if the variable was assigned a default value. """ - detector = set_by_cli.detector - if detector is None: + detector = set_by_cli.detector # type: ignore + if detector is None and helpful_parser is not None: # Setup on first run: `detector` is a weird version of config in which # the default value of every attribute is wrangled to be boolean-false plugins = plugins_disco.PluginsRegistry.find_all() # reconstructed_args == sys.argv[1:], or whatever was passed to main() reconstructed_args = helpful_parser.args + [helpful_parser.verb] - detector = set_by_cli.detector = prepare_and_parse_args( + detector = set_by_cli.detector = prepare_and_parse_args( # type: ignore plugins, reconstructed_args, detect_defaults=True) # propagate plugin requests: eg --standalone modifies config.authenticator - detector.authenticator, detector.installer = ( + detector.authenticator, detector.installer = ( # type: ignore plugin_selection.cli_plugin_requests(detector)) - logger.debug("Default Detector is %r", detector) if not isinstance(getattr(detector, var), _Default): + logger.debug("Var %s=%s (set by user).", var, getattr(detector, var)) return True for modifier in VAR_MODIFIERS.get(var, []): if set_by_cli(modifier): + logger.debug("Var %s=%s (set by user).", + var, VAR_MODIFIERS.get(var, [])) return True return False + # static housekeeping var -set_by_cli.detector = None +# functions attributed are not supported by mypy +# https://github.com/python/mypy/issues/2087 +set_by_cli.detector = None # type: ignore def has_default_value(option, value): @@ -222,8 +244,10 @@ :rtype: bool """ - return (option in helpful_parser.defaults and - helpful_parser.defaults[option] == value) + if helpful_parser is not None: + return (option in helpful_parser.defaults and + helpful_parser.defaults[option] == value) + return False def option_was_set(option, value): @@ -240,11 +264,12 @@ def argparse_type(variable): - "Return our argparse type function for a config variable (default: str)" + """Return our argparse type function for a config variable (default: str)""" # pylint: disable=protected-access - for action in helpful_parser.parser._actions: - if action.type is not None and action.dest == variable: - return action.type + if helpful_parser is not None: + for action in helpful_parser.parser._actions: + if action.type is not None and action.dest == variable: + return action.type return str def read_file(filename, mode="rb"): @@ -272,15 +297,17 @@ # 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 constants.CLI_DEFAULTS[name] + return copy.deepcopy(constants.CLI_DEFAULTS[name]) def config_help(name, hidden=False): """Extract the help message for an `.IConfig` attribute.""" + # pylint: disable=no-member if hidden: return argparse.SUPPRESS else: - return interfaces.IConfig[name].__doc__ + field = interfaces.IConfig.__getitem__(name) # type: zope.interface.interface.Attribute + return field.__doc__ class HelpfulArgumentGroup(object): @@ -318,23 +345,23 @@ # 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 "cerbot -h SUBCOMMAND" and "certbot -h all" +# 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 certs", + "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 cert is obtained", + "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 specifed with --cert-name)", + "short": "Renew all certificates (or one specified with --cert-name)", "opts": ("The 'renew' subcommand will attempt to renew all" " certificates (or more precisely, certificate lineages) you have" " previously obtained if they are close to expiry, and print a" @@ -346,7 +373,7 @@ " 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 NAME] [options]\n\n" + "usage": "\n\n certbot renew [--cert-name CERTNAME] [options]\n\n" }), ("certificates", { "short": "List certificates managed by Certbot", @@ -356,38 +383,58 @@ }), ("delete", { "short": "Clean up all files related to a certificate", - "opts": "Options for deleting 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", - "opts": "Options for revocation of certs", - "usage": "\n\n certbot revoke --cert-path /path/to/fullchain.pem [options]\n\n" + "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 & modification" + "opts": "Options for account registration & modification", + "usage": "\n\n certbot register --email user@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 cert in a server", - "opts": "Options for modifying how a cert is deployed" + "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" }), ("config_changes", { "short": "Show changes that Certbot has made to server configurations", - "opts": "Options for controlling which changes are displayed" + "opts": "Options for controlling which changes are displayed", + "usage": "\n\n certbot config_changes --num NUM [options]\n\n" }), ("rollback", { - "short": "Roll back server conf changes made during cert installation", - "opts": "Options for rolling back server configuration changes" + "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 for the "plugins" subcommand' + "opts": 'Options for for the "plugins" subcommand', + "usage": "\n\n certbot plugins [options]\n\n" }), ("update_symlinks", { "short": "Recreate symlinks in your /etc/letsencrypt/live/ directory", - "opts": ("Recreates cert and key symlinks in {0}, if you changed them by hand " + "opts": ("Recreates certificate and key symlinks in {0}, if you changed them by hand " "or edited a renewal configuration file".format( - os.path.join(flag_default("config_dir"), "live"))) + os.path.join(flag_default("config_dir"), "live"))), + "usage": "\n\n certbot update_symlinks [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" }), ] @@ -408,13 +455,14 @@ def __init__(self, args, plugins, detect_defaults=False): from certbot import main self.VERBS = { - "auth": main.obtain_cert, - "certonly": main.obtain_cert, + "auth": main.certonly, + "certonly": main.certonly, "config_changes": main.config_changes, "run": main.run, "install": main.install, "plugins": main.plugins_cmd, "register": main.register, + "unregister": main.unregister, "renew": main.renew, "revoke": main.revoke, "rollback": main.rollback, @@ -422,30 +470,47 @@ "update_symlinks": main.update_symlinks, "certificates": main.certificates, "delete": main.delete, + "enhance": main.enhance, } + # Get notification function for printing + try: + self.notify = zope.component.getUtility( + interfaces.IDisplay).notification + except zope_interfaces.ComponentLookupError: + self.notify = display_util.NoninteractiveDisplay( + sys.stdout).notification + + # List of topics for which additional help can be provided - HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) - HELP_TOPICS += self.COMMANDS_TOPICS + ["manage"] + HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + HELP_TOPICS += list(self.VERBS) + self.COMMANDS_TOPICS + ["manage"] plugin_names = list(plugins) - self.help_topics = HELP_TOPICS + plugin_names + [None] + self.help_topics = HELP_TOPICS + plugin_names + [None] # type: ignore self.detect_defaults = detect_defaults 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) 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 + self.help_arg = help1 if isinstance(help1, six.string_types) else help2 short_usage = self._usage_string(plugins, self.help_arg) self.visible_topics = self.determine_help_topics(self.help_arg) - self.groups = {} # elements are added by .add_group() - self.defaults = {} # elements are added by .parse_args() + + # elements are added by .add_group() + self.groups = {} # type: Dict[str, argparse._ArgumentGroup] + # elements are added by .parse_args() + self.defaults = {} # type: Dict[str, Any] self.parser = configargparse.ArgParser( prog="certbot", @@ -487,14 +552,14 @@ if "apache" in plugins: apache_doc = "--apache Use the Apache plugin for authentication & installation" else: - apache_doc = "(the cerbot apache plugin is not installed)" + apache_doc = "(the certbot apache plugin is not installed)" usage = SHORT_USAGE if help_arg == True: - print(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_USAGE) + self.notify(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_USAGE) sys.exit(0) elif help_arg in self.COMMANDS_TOPICS: - print(usage + self._list_subcommands()) + 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 @@ -506,6 +571,13 @@ return usage + def remove_config_file_domains_for_renewal(self, parsed_args): + """Make "certbot renew" safe if domains are set in cli.ini.""" + # Works around https://github.com/certbot/certbot/issues/4096 + if self.verb == "renew": + for source, flags in self.parser._source_to_settings.items(): # pylint: disable=protected-access + if source.startswith("config_file") and "domains" in flags: + parsed_args.domains = _Default() if self.detect_defaults else [] def parse_args(self): """Parses command line arguments and returns the result. @@ -518,6 +590,8 @@ parsed_args.func = self.VERBS[self.verb] parsed_args.verb = self.verb + self.remove_config_file_domains_for_renewal(parsed_args) + if self.detect_defaults: return parsed_args @@ -547,6 +621,20 @@ if parsed_args.must_staple: parsed_args.staple = True + if parsed_args.validate_hooks: + hooks.validate_hooks(parsed_args) + + if parsed_args.allow_subset_of_names: + if any(util.is_wildcard_domain(d) for d in parsed_args.domains): + raise errors.Error("Using --allow-subset-of-names with a" + " wildcard domain is not supported.") + + if parsed_args.hsts and parsed_args.auto_hsts: + raise errors.Error( + "Parameters --hsts and --auto-hsts cannot be used simultaneously.") + + possible_deprecation_warning(parsed_args) + return parsed_args def set_test_server(self, parsed_args): @@ -596,7 +684,9 @@ % parsed_args.csr[0]) parsed_args.actual_csr = (csr, typ) - csr_domains, config_domains = set(domains), set(parsed_args.domains) + + csr_domains = set([d.lower() for d in domains]) + config_domains = set(parsed_args.domains) if csr_domains != config_domains: raise errors.ConfigurationError( "Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}" @@ -736,7 +826,6 @@ 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): @@ -774,11 +863,11 @@ return dict([(t, t == chosen_topic) for t in self.help_topics]) def _add_all_groups(helpful): - helpful.add_group("automation", description="Arguments for automating execution & other tweaks") + 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="Arguments changing execution paths & servers") + 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", "update_symlinks"]) @@ -813,48 +902,72 @@ "e.g. -vvv.") helpful.add( None, "-t", "--text", dest="text_mode", action="store_true", - help=argparse.SUPPRESS) + 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, "automation", "run", "certonly"], "-n", "--non-interactive", "--noninteractive", + [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"], + [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"], + [None, "run", "certonly", "certificates", "enhance"], "-d", "--domains", "--domain", dest="domains", - metavar="DOMAIN", action=_DomainsAction, default=[], + metavar="DOMAIN", action=_DomainsAction, + default=flag_default("domains"), help="Domain names to apply. For multiple domains you can use " "multiple -d flags or enter a comma separated list of domains " - "as a parameter. (default: Ask)") - helpful.add( - [None, "run", "certonly", "manage", "delete", "certificates"], - "--cert-name", dest="certname", - metavar="CERTNAME", default=None, - help="Certificate name to apply. Only one certificate name can be used " - "per Certbot run. To see certificate names, run 'certbot certificates'. " - "When creating a new certificate, specifies the new certificate's name.") + "as a parameter. The first domain provided will be the " + "subject CN of the certificate, and all domains will be " + "Subject Alternative Names on the certificate. " + "The first domain will also be used in " + "some software user interfaces and as the file paths for the " + "certificate and related material unless otherwise " + "specified or you already have a certificate with the same " + "name. In the case of a name collision it will append a number " + "like 0001 to the file path name. (default: Ask)") + helpful.add( + [None, "run", "certonly", "manage", "delete", "certificates", + "renew", "enhance"], "--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. " + "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", - help="Perform a test run of the client, obtaining test (invalid) certs" + default=flag_default("dry_run"), + help="Perform a test run of the client, obtaining test (invalid) certificates" " but not saving them to disk. This can currently only be used" " with the 'certonly' and 'renew' subcommands. \nNote: Although --dry-run" " tries to avoid making any persistent changes on a system, it " " is not completely side-effect free: if used with webserver authenticator plugins" " like apache and nginx, it makes and then reverts temporary config changes" - " in order to obtain test certs, and reloads webservers to deploy and then" + " in order to obtain test certificates, and reloads webservers to deploy and then" " roll back those changes. It also calls --pre-hook and --post-hook commands" " if they are defined because they may be necessary to accurately simulate" - " renewal. --renew-hook commands are not called.") + " renewal. --deploy-hook commands are not called.") 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 in the " "event of key loss or account compromise you will irrevocably " @@ -865,20 +978,30 @@ "update to the web site.") helpful.add( "register", "--update-registration", action="store_true", + default=flag_default("update_registration"), help="With the register verb, indicates that details associated " "with an existing registration, such as the e-mail address, " "should be updated, rather than registering a new account.") - helpful.add(["register", "automation"], "-m", "--email", help=config_help("email")) + helpful.add( + ["register", "unregister", "automation"], "-m", "--email", + default=flag_default("email"), + help=config_help("email")) + helpful.add(["register", "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", "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", - help="If the requested cert matches an existing cert, always keep the " + 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 cert). (default: Ask)") + "'run' subcommand this means reinstall the existing certificate). (default: Ask)") helpful.add( - "automation", "--expand", action="store_true", - help="If an existing cert covers some subset of the requested names, " + "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", @@ -886,21 +1009,30 @@ help="show program's version number and exit") helpful.add( ["automation", "renew"], - "--force-renewal", "--renew-by-default", - action="store_true", dest="renew_by_default", help="If a certificate " + "--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", - action="store_true", dest="renew_with_new_domains", help="If a " + "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", "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 " @@ -908,37 +1040,54 @@ "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( - "automation", "--account", metavar="ACCOUNT_ID", + ["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", "--os-packages-only", action="store_true", + default=flag_default("os_packages_only"), help="(certbot-auto only) install OS package dependencies and then stop") helpful.add( "automation", "--no-self-upgrade", action="store_true", + default=flag_default("no_self_upgrade"), help="(certbot-auto only) prevent the certbot-auto script from" " upgrading itself to newer released versions (default: Upgrade" " automatically)") helpful.add( + "automation", "--no-bootstrap", action="store_true", + default=flag_default("no_bootstrap"), + help="(certbot-auto only) prevent the certbot-auto script from" + " installing OS-level dependencies (default: Prompt to install " + " OS-wide dependencies, but exit if the user says 'No')") + 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", - action='store_true', dest='staging', - help='Use the staging server to obtain or revoke test (invalid) certs; equivalent' - ' to --server ' + constants.STAGING_URI) + dest="staging", action="store_true", default=flag_default("staging"), + help="Use the staging server to obtain or revoke test (invalid) certificates; equivalent" + " to --server " + constants.STAGING_URI) helpful.add( - "testing", "--debug", action="store_true", + "testing", "--debug", action="store_true", default=flag_default("debug"), help="Show tracebacks in case of errors, and allow certbot-auto " "execution on experimental platforms") 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") + helpful.add( "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) @@ -947,59 +1096,75 @@ default=flag_default("tls_sni_01_port"), help=config_help("tls_sni_01_port")) helpful.add( + ["testing", "standalone"], "--tls-sni-01-address", + default=flag_default("tls_sni_01_address"), + help=config_help("tls_sni_01_address")) + 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", "--break-my-certs", action="store_true", - help="Be willing to replace or renew valid certs with invalid " - "(testing/staging) certs") + 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", "--must-staple", action="store_true", - help=config_help("must_staple"), dest="must_staple", default=False) + dest="must_staple", default=flag_default("must_staple"), + help=config_help("must_staple")) helpful.add( - "security", "--redirect", action="store_true", + ["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: Ask)", dest="redirect", default=None) + "authenticated vhost. (default: Ask)") helpful.add( - "security", "--no-redirect", action="store_false", + "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: Ask)", dest="redirect", default=None) + "authenticated vhost. (default: Ask)") helpful.add( - "security", "--hsts", action="store_true", + ["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.", dest="hsts", default=False) + " Defends against SSL Stripping.") helpful.add( - "security", "--no-hsts", action="store_false", - help=argparse.SUPPRESS, dest="hsts", default=False) + "security", "--no-hsts", action="store_false", dest="hsts", + default=flag_default("hsts"), help=argparse.SUPPRESS) helpful.add( - "security", "--uir", action="store_true", - help="Add the \"Content-Security-Policy: upgrade-insecure-requests\"" - " header to every HTTP response. Forcing the browser to use" - " https:// for every http:// resource.", dest="uir", default=None) + ["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", - help=argparse.SUPPRESS, dest="uir", default=None) + "security", "--no-uir", action="store_false", dest="uir", default=flag_default("uir"), + help=argparse.SUPPRESS) helpful.add( - "security", "--staple-ocsp", action="store_true", + "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.", - dest="staple", default=None) + " the certificate that the server offers during TLS.") helpful.add( - "security", "--no-staple-ocsp", action="store_false", - help=argparse.SUPPRESS, dest="staple", default=None) + "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( ["manual", "standalone", "certonly", "renew"], "--preferred-challenges", dest="pref_challs", - action=_PrefChallAction, default=[], + 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 "tls-sni-01,http,dns"). ' @@ -1024,27 +1189,54 @@ " 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", "--renew-hook", + action=_RenewHookAction, help=argparse.SUPPRESS) helpful.add( - "renew", "--renew-hook", - help="Command to be run in a shell once for each successfully renewed" - " certificate. For this command, the shell variable $RENEWED_LINEAGE" - " will point to the config live subdirectory containing the new certs" - " and keys; the shell variable $RENEWED_DOMAINS will contain a" - " space-delimited list of renewed cert domains") + "renew", "--deploy-hook", action=_DeployHookAction, + help='Command to be run in a shell once for each successfully' + ' issued certificate. 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=True, + action="store_false", dest="validate_hooks", + default=flag_default("validate_hooks"), help="Ordinarily the commands specified for" - " --pre-hook/--post-hook/--renew-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." + " --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.") helpful.add_deprecated_argument("--agree-dev-preview", 0) helpful.add_deprecated_argument("--dialog", 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 @@ -1058,68 +1250,108 @@ def _create_subparsers(helpful): - helpful.add("config_changes", "--num", type=int, + helpful.add("config_changes", "--num", type=int, default=flag_default("num"), help="How many past revisions you want to be displayed") from certbot.client import sample_user_agent # avoid import loops helpful.add( - None, "--user-agent", default=None, - 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 and " - "plugin. If you wish to hide your server OS version from the Let's " + 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})'.format(sample_user_agent())) + '(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", type=read_file, + "--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(sorted(constants.REVOCATION_REASONS, + key=constants.REVOCATION_REASONS.get)), + 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.") + 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", help="Initialize plugins.") + "--init", action="store_true", default=flag_default("init"), + help="Initialize plugins.") helpful.add("plugins", - "--prepare", action="store_true", help="Initialize and prepare 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.IAuthenticator, help="Limit to authenticator plugins only.") helpful.add("plugins", "--installers", action="append_const", dest="ifaces", + default=flag_default("ifaces"), const=interfaces.IInstaller, help="Limit to installer plugins only.") +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): + return super(CaseInsensitiveList, self).__contains__(element.lower()) + + def _paths_parser(helpful): add = helpful.add verb = helpful.verb if verb == "help": verb = helpful.help_arg - cph = "Path to where cert is saved (with auth --csr), installed from, or revoked." - section = ["paths", "install", "revoke", "certonly", "manage"] + cph = "Path to where certificate is saved (with auth --csr), installed from, or revoked." + sections = ["paths", "install", "revoke", "certonly", "manage"] if verb == "certonly": - add(section, "--cert-path", type=os.path.abspath, + add(sections, "--cert-path", type=os.path.abspath, default=flag_default("auth_cert_path"), help=cph) elif verb == "revoke": - add(section, "--cert-path", type=read_file, required=True, help=cph) + add(sections, "--cert-path", type=read_file, required=False, help=cph) else: - add(section, "--cert-path", type=os.path.abspath, - help=cph, required=(verb == "install")) + add(sections, "--cert-path", type=os.path.abspath, help=cph) section = "paths" if verb in ("install", "revoke"): section = verb # revoke --key-path reads a file, install --key-path takes a string - add(section, "--key-path", required=(verb == "install"), + add(section, "--key-path", type=((verb == "revoke" and read_file) or os.path.abspath), - help="Path to private key for cert installation " + 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(["install", "paths"], "--fullchain-path", default=default_cp, type=os.path.abspath, - help="Accompanying path to a full certificate chain (cert plus chain).") + 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", "--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"), @@ -1141,24 +1373,84 @@ "a particular plugin by setting options provided below. Running " "--help will list flags specific to that plugin.") - helpful.add("plugins", "--configurator", + 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", "-a", "--authenticator", help="Authenticator plugin name.") - helpful.add("plugins", "-i", "--installer", + helpful.add("plugins", "-a", "--authenticator", default=flag_default("authenticator"), + help="Authenticator plugin name.") + helpful.add("plugins", "-i", "--installer", default=flag_default("installer"), help="Installer plugin name (also used to find domains).") helpful.add(["plugins", "certonly", "run", "install", "config_changes"], - "--apache", action="store_true", - help="Obtain and install certs using Apache") + "--apache", action="store_true", default=flag_default("apache"), + help="Obtain and install certificates using Apache") helpful.add(["plugins", "certonly", "run", "install", "config_changes"], - "--nginx", action="store_true", help="Obtain and install certs using Nginx") + "--nginx", action="store_true", default=flag_default("nginx"), + help="Obtain and install certificates using Nginx") helpful.add(["plugins", "certonly"], "--standalone", action="store_true", - help='Obtain certs using a "standalone" webserver.') + default=flag_default("standalone"), + help='Obtain certificates using a "standalone" webserver.') helpful.add(["plugins", "certonly"], "--manual", action="store_true", - help='Provide laborious manual instructions for obtaining a cert') + default=flag_default("manual"), + help="Provide laborious manual instructions for obtaining a certificate") helpful.add(["plugins", "certonly"], "--webroot", action="store_true", - help='Obtain certs by placing files in a webroot directory.') + 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-cloudxns", action="store_true", + default=flag_default("dns_cloudxns"), + help=("Obtain certificates using a DNS TXT record (if you are " + "using CloudXNS 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 Infrastracture 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 @@ -1167,6 +1459,15 @@ helpful.add_plugin_args(plugins) +class _EncodeReasonAction(argparse.Action): + """Action class for parsing revocation reason.""" + + def __call__(self, parser, namespace, reason, option_string=None): + """Encodes the reason for certificate revocation.""" + code = constants.REVOCATION_REASONS[reason.lower()] + setattr(namespace, self.dest, code) + + class _DomainsAction(argparse.Action): """Action class for parsing domains.""" @@ -1205,7 +1506,7 @@ try: challs = parse_preferred_challenges(pref_challs.split(",")) except errors.Error as error: - raise argparse.ArgumentTypeError(str(error)) + raise argparse.ArgumentError(self, str(error)) namespace.pref_challs.extend(challs) @@ -1230,3 +1531,53 @@ raise errors.Error( "Unrecognized challenges: {0}".format(unrecognized)) return challs + +def _user_agent_comment_type(value): + if "(" in value or ")" in value: + raise argparse.ArgumentTypeError("may not contain parentheses") + return value + +class _DeployHookAction(argparse.Action): + """Action class for parsing deploy hooks.""" + + def __call__(self, parser, namespace, values, option_string=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, namespace, values, option_string=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): + """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 diff -Nru python-certbot-0.10.2/certbot/client.py python-certbot-0.28.0/certbot/client.py --- python-certbot-0.10.2/certbot/client.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/client.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,29 +1,39 @@ """Certbot client API.""" +import datetime import logging import os +import platform + from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import rsa +# https://github.com/python/typeshed/blob/master/third_party/ +# 2/cryptography/hazmat/primitives/asymmetric/rsa.pyi +from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key # type: ignore +import josepy as jose import OpenSSL import zope.component from acme import client as acme_client -from acme import jose +from acme import crypto_util as acme_crypto_util +from acme import errors as acme_errors from acme import messages +from acme.magic_typing import Optional # pylint: disable=unused-import,no-name-in-module import certbot from certbot import account from certbot import auth_handler +from certbot import cli +from certbot import compat from certbot import constants from certbot import crypto_util -from certbot import errors +from certbot import eff from certbot import error_handler +from certbot import errors from certbot import interfaces -from certbot import util from certbot import reverter from certbot import storage -from certbot import cli +from certbot import util from certbot.display import ops as display_ops from certbot.display import enhancements @@ -33,12 +43,12 @@ logger = logging.getLogger(__name__) -def acme_from_config_key(config, key): +def acme_from_config_key(config, key, regr=None): "Wrangle ACME client construction" # TODO: Allow for other alg types besides RS256 - net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl), + net = acme_client.ClientNetwork(key, account=regr, verify_ssl=(not config.no_verify_ssl), user_agent=determine_user_agent(config)) - return acme_client.Client(config.server, key=key, net=net) + return acme_client.BackwardsCompatibleClientV2(net, key, config.server) def determine_user_agent(config): @@ -50,22 +60,62 @@ :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}) Authenticator/{2} Installer/{3}" - ua = ua.format(certbot.__version__, util.get_os_info_ua(), - config.authenticator, config.installer) + 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(-auto)" + 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): + "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(object): + "Shim for computing a sample user agent." + def __init__(self): + self.authenticator = "XXX" + self.installer = "YYY" + self.user_agent = None + self.verb = "SUBCOMMAND" + + def __getattr__(self, name): + "Any config properties we might have are None." + return None + def sample_user_agent(): "Document what this Certbot's user agent string will be like." - class DummyConfig(object): - "Shim for computing a sample user agent." - def __init__(self): - self.authenticator = "XXX" - self.installer = "YYY" - self.user_agent = None + return determine_user_agent(DummyConfig()) @@ -93,7 +143,7 @@ Terms of Service present in the contained `.Registration.terms_of_service` is accepted by the client, and ``False`` otherwise. ``tos_cb`` will be called only if the - client acction is necessary, i.e. when ``terms_of_service is not + 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! @@ -116,47 +166,46 @@ logger.warning(msg) raise errors.Error(msg) if not config.dry_run: - logger.warning("Registering without email!") + logger.info("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 - key = jose.JWKRSA(key=jose.ComparableRSAKey( - rsa.generate_private_key( + rsa_key = generate_private_key( public_exponent=65537, key_size=config.rsa_key_size, - backend=default_backend()))) + 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) - - if regr.terms_of_service is not None: - if tos_cb is not None and not tos_cb(regr): - raise errors.Error( - "Registration cannot proceed without accepting " - "Terms of Service.") - regr = acme.agree_to_tos(regr) + regr = perform_registration(acme, config, tos_cb) acc = account.Account(regr, key) - account.report_new_account(acc, config) - account_storage.save(acc) + account.report_new_account(config) + account_storage.save(acc, acme) + + eff.handle_subscription(config) return acc, acme -def perform_registration(acme, config): +def perform_registration(acme, config, tos_cb): """ Actually register new account, trying repeatedly if there are email problems - :param .IConfig config: Client configuration. :param acme.client.Client client: ACME client object. + :param .IConfig config: Client configuration. + :param Callable tos_cb: a callback to handle Term of Service agreement. :returns: Registration Resource. :rtype: `acme.messages.RegistrationResource` - - :raises .UnexpectedUpdate: """ try: - return acme.register(messages.NewRegistration.from_data(email=config.email)) + return acme.new_account_and_tos(messages.NewRegistration.from_data(email=config.email), + tos_cb) except messages.Error as e: if e.code == "invalidEmail" or e.code == "invalidContact": if config.noninteractive_mode: @@ -165,14 +214,14 @@ "registration again." % config.email) raise errors.Error(msg) else: - config.namespace.email = display_ops.get_email(invalid=True) - return perform_registration(acme, config) + config.email = display_ops.get_email(invalid=True) + return perform_registration(acme, config, tos_cb) else: raise class Client(object): - """ACME protocol client. + """Certbot's client. :ivar .IConfig config: Client configuration. :ivar .Account account: Account registered with `register`. @@ -182,8 +231,8 @@ :ivar .IAuthenticator auth: Prepared (`.IAuthenticator.prepare`) authenticator that can solve ACME challenges. :ivar .IInstaller installer: Installer. - :ivar acme.client.Client acme: Optional ACME client API handle. - You might already have one from `register`. + :ivar acme.client.BackwardsCompatibleClientV2 acme: Optional ACME + client API handle. You might already have one from `register`. """ @@ -196,7 +245,7 @@ # 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) + acme = acme_from_config_key(config, self.account.key, self.account.regr) self.acme = acme if auth is not None: @@ -205,22 +254,15 @@ else: self.auth_handler = None - def obtain_certificate_from_csr(self, domains, csr, - typ=OpenSSL.crypto.FILETYPE_ASN1, authzr=None): + def obtain_certificate_from_csr(self, csr, orderr=None): """Obtain certificate. - Internal function with precondition that `domains` are - consistent with identifiers present in the `csr`. - - :param list domains: Domain names. - :param .util.CSR csr: DER-encoded Certificate Signing + :param .util.CSR csr: PEM-encoded Certificate Signing Request. The key used to generate this CSR can be different than `authkey`. - :param list authzr: List of - :class:`acme.messages.AuthorizationResource` + :param acme.messages.OrderResource orderr: contains authzrs - :returns: `.CertificateResource` and certificate chain (as - returned by `.fetch_chain`). + :returns: certificate and chain as PEM byte strings :rtype: tuple """ @@ -232,46 +274,104 @@ if self.account.regr is None: raise errors.Error("Please register with the ACME server first.") - logger.debug("CSR: %s, domains: %s", csr, domains) + logger.debug("CSR: %s", csr) - if authzr is None: - authzr = self.auth_handler.get_authorizations(domains) + if orderr is None: + orderr = self._get_order_and_authorizations(csr.data, best_effort=False) - certr = self.acme.request_issuance( - jose.ComparableX509( - OpenSSL.crypto.load_certificate_request(typ, csr.data)), - authzr) - return certr, self.acme.fetch_chain(certr) + deadline = datetime.datetime.now() + datetime.timedelta(seconds=90) + orderr = self.acme.finalize_order(orderr, deadline) + cert, chain = crypto_util.cert_and_chain_from_fullchain(orderr.fullchain_pem) + return cert.encode(), chain.encode() - def obtain_certificate(self, domains): + def obtain_certificate(self, domains, old_keypath=None): """Obtains a certificate from the ACME server. `.register` must be called before `.obtain_certificate` :param list domains: domains to get a certificate - :returns: `.CertificateResource`, certificate chain (as - returned by `.fetch_chain`), and newly generated private key - (`.util.Key`) and DER-encoded Certificate Signing Request - (`.util.CSR`). + :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 """ - authzr = self.auth_handler.get_authorizations( - domains, - self.config.allow_subset_of_names) - auth_domains = set(a.body.identifier.value for a in authzr) - domains = [d for d in domains if d in auth_domains] + # 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 = util.Key(file=keypath, pem=keypem) # type: Optional[util.Key] + 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 # Create CSR from names - key = crypto_util.init_save_key( - self.config.rsa_key_size, self.config.key_dir) - csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) + if self.config.dry_run: + key = key or util.Key(file=None, + pem=crypto_util.make_key(self.config.rsa_key_size)) + 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.init_save_key(self.config.rsa_key_size, + self.config.key_dir) + csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) - return (self.obtain_certificate_from_csr(domains, csr, authzr=authzr) - + (key, csr)) + orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names) + authzr = orderr.authorizations + auth_domains = set(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: + if not self.config.dry_run: + os.remove(key.file) + os.remove(csr.file) + return self.obtain_certificate(successful_domains) + else: + cert, chain = self.obtain_certificate_from_csr(csr, orderr) + + return cert, chain, key, csr + + def _get_order_and_authorizations(self, csr_pem, best_effort): + """Request a new order and complete its authorizations. + + :param str 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 + + """ + 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.") + authzr = self.auth_handler.handle_authorizations(orderr, best_effort) + return orderr.update(authorizations=authzr) + + # pylint: disable=no-member def obtain_and_enroll_certificate(self, domains, certname): """Obtain and enroll certificate. @@ -279,43 +379,62 @@ authenticator and installer, and then create a new renewable lineage containing it. - :param list domains: Domains to request. - :param plugins: A PluginsFactory object. - :param str certname: Name of new cert + :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.storage.RenewableCert` instance referred to the enrolled cert lineage, False if the cert could not be obtained, or None if doing a successful dry run. """ - certr, chain, key, _ = self.obtain_certificate(domains) + 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.warning( + logger.info( "Non-standard path(s), might not work with crontab installed " "by your operating system package manager") - new_name = certname if certname else domains[0] + new_name = self._choose_lineagename(domains, certname) + if self.config.dry_run: logger.debug("Dry run: Skipping creating new lineage for %s", new_name) return None else: return storage.RenewableCert.new_lineage( - new_name, OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped), - key.pem, crypto_util.dump_pyopenssl_chain(chain), + new_name, cert, + key.pem, chain, self.config) - def save_certificate(self, certr, chain_cert, + def _choose_lineagename(self, domains, certname): + """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 + + """ + if certname: + return certname + elif util.is_wildcard_domain(domains[0]): + # Don't make files and directories starting with *. + return domains[0][2:] + else: + return domains[0] + + def save_certificate(self, cert_pem, chain_pem, cert_path, chain_path, fullchain_path): """Saves the certificate received from the ACME server. - :param certr: ACME "certificate" resource. - :type certr: :class:`acme.messages.Certificate` - - :param list chain_cert: + :param str cert_pem: + :param str 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. @@ -329,11 +448,9 @@ """ for path in cert_path, chain_path, fullchain_path: util.make_or_verify_dir( - os.path.dirname(path), 0o755, os.geteuid(), + os.path.dirname(path), 0o755, compat.os_geteuid(), self.config.strict_permissions) - cert_pem = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path) @@ -344,20 +461,15 @@ logger.info("Server issued certificate; certificate written to %s", abs_cert_path) - if not chain_cert: - return abs_cert_path, None, None - else: - chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert) + chain_file, abs_chain_path =\ + _open_pem_file('chain_path', chain_path) + fullchain_file, abs_fullchain_path =\ + _open_pem_file('fullchain_path', fullchain_path) - chain_file, abs_chain_path =\ - _open_pem_file('chain_path', chain_path) - fullchain_file, abs_fullchain_path =\ - _open_pem_file('fullchain_path', fullchain_path) + _save_chain(chain_pem, chain_file) + _save_chain(cert_pem + chain_pem, fullchain_file) - _save_chain(chain_pem, chain_file) - _save_chain(cert_pem + chain_pem, fullchain_file) - - return abs_cert_path, abs_chain_path, abs_fullchain_path + return abs_cert_path, abs_chain_path, abs_fullchain_path def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path): @@ -395,7 +507,7 @@ # sites may have been enabled / final cleanup self.installer.restart() - def enhance_config(self, domains, chain_path): + def enhance_config(self, domains, chain_path, ask_redirect=True): """Enhance the configuration. :param list domains: list of domains to configure @@ -422,8 +534,9 @@ for config_name, enhancement_name, option in enhancement_info: config_value = getattr(self.config, config_name) if enhancement_name in supported: - if config_name == "redirect" and config_value is None: - config_value = enhancements.ask(enhancement_name) + if ask_redirect: + if config_name == "redirect" and config_value is None: + config_value = enhancements.ask(enhancement_name) if config_value: self.apply_enhancement(domains, enhancement_name, option) enhanced = True @@ -438,17 +551,13 @@ self.installer.restart() def apply_enhancement(self, domains, enhancement, options=None): - """Applies an enhacement on all domains. - - :param domains: list of ssl_vhosts - :type list of str + """Applies an enhancement on all domains. - :param enhancement: name of enhancement, e.g. ensure-http-header - :type str + :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 need make options a list. - :param options: options to enhancement, e.g. Strict-Transport-Security - :type str + .. 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. @@ -463,8 +572,12 @@ try: self.installer.enhance(dom, enhancement, options) except errors.PluginEnhancementAlreadyPresent: - logger.warning("Enhancement %s was already set.", - enhancement) + if enhancement == "ensure-http-header": + logger.warning("Enhancement %s was already set.", + options) + else: + logger.warning("Enhancement %s was already set.", + enhancement) except errors.PluginError: logger.warning("Unable to set enhancement %s for %s", enhancement, dom) @@ -494,11 +607,11 @@ self.installer.rollback_checkpoints() self.installer.restart() except: - # TODO: suggest letshelp-letsencypt here reporter.add_message( "An error occurred and we failed to restore your config and " - "restart your server. Please submit a bug report to " - "https://github.com/letsencrypt/letsencrypt", + "restart your server. Please post to " + "https://community.letsencrypt.org/c/server-config " + "with details about your configuration and this error you received.", reporter.HIGH_PRIORITY) raise reporter.add_message(success_msg, reporter.HIGH_PRIORITY) @@ -533,8 +646,10 @@ if csr.form == "der": csr_obj = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, csr.data) - csr = util.CSR(csr.file, OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, csr_obj), "pem") + 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): diff -Nru python-certbot-0.10.2/certbot/colored_logging.py python-certbot-0.28.0/certbot/colored_logging.py --- python-certbot-0.10.2/certbot/colored_logging.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/colored_logging.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -"""A formatter and StreamHandler for colorizing logging output.""" -import logging -import sys - -from certbot import util - - -class StreamHandler(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=None): - if sys.version_info < (2, 7): - # pragma: no cover - # pylint: disable=non-parent-init-called - logging.StreamHandler.__init__(self, stream) - else: - super(StreamHandler, self).__init__(stream) - self.colored = (sys.stderr.isatty() if stream is None else - stream.isatty()) - self.red_level = logging.WARNING - - def format(self, record): - """Formats the string representation of record. - - :param logging.LogRecord record: Record to be formatted - - :returns: Formatted, string representation of record - :rtype: str - - """ - out = (logging.StreamHandler.format(self, record) - if sys.version_info < (2, 7) - else super(StreamHandler, self).format(record)) - if self.colored and record.levelno >= self.red_level: - return ''.join((util.ANSI_SGR_RED, out, util.ANSI_SGR_RESET)) - else: - return out diff -Nru python-certbot-0.10.2/certbot/compat.py python-certbot-0.28.0/certbot/compat.py --- python-certbot-0.10.2/certbot/compat.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/compat.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,174 @@ +""" +Compatibility layer to run certbot both on Linux and Windows. + +The approach used here is similar to Modernizr for Web browsers. +We do not check the platform type to determine if a particular logic is supported. +Instead, we apply a logic, and then fallback to another logic if first logic +is not supported at runtime. + +Then logic chains are abstracted into single functions to be exposed to certbot. +""" +import os +import select +import sys +import errno +import ctypes +import stat + +from certbot import errors + +try: + # Linux specific + import fcntl # pylint: disable=import-error +except ImportError: + # Windows specific + import msvcrt # pylint: disable=import-error + +UNPRIVILEGED_SUBCOMMANDS_ALLOWED = [ + 'certificates', 'enhance', 'revoke', 'delete', + 'register', 'unregister', 'config_changes', 'plugins'] +def raise_for_non_administrative_windows_rights(subcommand): + """ + On Windows, raise if current shell does not have the administrative rights. + Do nothing on Linux. + + :param str subcommand: The subcommand (like 'certonly') passed to the certbot client. + + :raises .errors.Error: If the provided subcommand must be run on a shell with + administrative rights, and current shell does not have these rights. + + """ + # Why not simply try ctypes.windll.shell32.IsUserAnAdmin() and catch AttributeError ? + # Because windll exists only on a Windows runtime, and static code analysis engines + # do not like at all non existent objects when run from Linux (even if we handle properly + # all the cases in the code). + # So we access windll only by reflection to trick theses engines. + if hasattr(ctypes, 'windll') and subcommand not in UNPRIVILEGED_SUBCOMMANDS_ALLOWED: + windll = getattr(ctypes, 'windll') + if windll.shell32.IsUserAnAdmin() == 0: + raise errors.Error( + 'Error, "{0}" subcommand must be run on a shell with administrative rights.' + .format(subcommand)) + +def os_geteuid(): + """ + Get current user uid + + :returns: The current user uid. + :rtype: int + + """ + try: + # Linux specific + return os.geteuid() + except AttributeError: + # Windows specific + return 0 + +def os_rename(src, dst): + """ + 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. + """ + try: + os.rename(src, dst) + except OSError as err: + # Windows specific, renaming a file on an existing path is not possible. + # On Python 3, the best fallback with atomic capabilities we have is os.replace. + if err.errno != errno.EEXIST: + # Every other error is a legitimate exception. + raise + if not hasattr(os, 'replace'): # pragma: no cover + # We should never go on this line. Either we are on Linux and os.rename has succeeded, + # either we are on Windows, and only Python >= 3.4 is supported where os.replace is + # available. + raise RuntimeError('Error: tried to run os_rename on Python < 3.3. ' + 'Certbot supports only Python 3.4 >= on Windows.') + getattr(os, 'replace')(src, dst) + + +def readline_with_timeout(timeout, prompt): + """ + 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)) + 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() + +def lock_file(fd): + """ + Lock the file linked to the specified file descriptor. + + :param int fd: The file descriptor of the file to lock. + + """ + if 'fcntl' in sys.modules: + # Linux specific + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + else: + # Windows specific + msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) + +def release_locked_file(fd, path): + """ + Remove, close, and release a lock file specified by its file descriptor and its path. + + :param int fd: The file descriptor of the lock file. + :param str path: The path of the lock file. + + """ + # Linux specific + # + # 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(path) + except OSError as err: + if err.errno == errno.EACCES: + # Windows specific + # We will not be able to remove a file before closing it. + # To avoid race conditions described for Linux, we will not delete the lockfile, + # just close it to be reused on the next Certbot call. + pass + else: + raise + finally: + os.close(fd) + +def compare_file_modes(mode1, mode2): + """Return true if the two modes can be considered as equals for this platform""" + if 'fcntl' in sys.modules: + # Linux specific: standard compare + return oct(stat.S_IMODE(mode1)) == oct(stat.S_IMODE(mode2)) + # Windows specific: most of mode bits are ignored on Windows. Only check user R/W rights. + return (stat.S_IMODE(mode1) & stat.S_IREAD == stat.S_IMODE(mode2) & stat.S_IREAD + and stat.S_IMODE(mode1) & stat.S_IWRITE == stat.S_IMODE(mode2) & stat.S_IWRITE) diff -Nru python-certbot-0.10.2/certbot/configuration.py python-certbot-0.28.0/certbot/configuration.py --- python-certbot-0.10.2/certbot/configuration.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/configuration.py 2018-11-07 21:14:56.000000000 +0000 @@ -42,7 +42,7 @@ """ def __init__(self, namespace): - self.namespace = namespace + object.__setattr__(self, 'namespace', namespace) self.namespace.config_dir = os.path.abspath(self.namespace.config_dir) self.namespace.work_dir = os.path.abspath(self.namespace.work_dir) @@ -54,6 +54,9 @@ def __getattr__(self, name): return getattr(self.namespace, name) + def __setattr__(self, name, value): + setattr(self.namespace, name, value) + @property def server_path(self): """File path based on ``server``.""" @@ -62,8 +65,12 @@ @property def accounts_dir(self): # pylint: disable=missing-docstring + return self.accounts_dir_for_server_path(self.server_path) + + def accounts_dir_for_server_path(self, server_path): + """Path to accounts directory based on server_path""" return os.path.join( - self.namespace.config_dir, constants.ACCOUNTS_DIR, self.server_path) + self.namespace.config_dir, constants.ACCOUNTS_DIR, server_path) @property def backup_dir(self): # pylint: disable=missing-docstring @@ -105,6 +112,30 @@ return os.path.join( self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR) + @property + def renewal_hooks_dir(self): + """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): + """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): + """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): + """Path to the post-hook directory for the renew subcommand.""" + return os.path.join(self.renewal_hooks_dir, + constants.RENEWAL_POST_HOOKS_DIR) + def check_config_sanity(config): """Validate command line options and display error message if diff -Nru python-certbot-0.10.2/certbot/constants.py python-certbot-0.28.0/certbot/constants.py --- python-certbot-0.10.2/certbot/constants.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/constants.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,6 +1,7 @@ """Certbot constants.""" -import os import logging +import os +import pkg_resources from acme import challenges @@ -18,22 +19,114 @@ os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), ], + + # Main parser verbose_count=-int(logging.INFO / 10), - server="https://acme-v01.api.letsencrypt.org/directory", + text_mode=False, + max_log_backups=1000, + noninteractive_mode=False, + force_interactive=False, + domains=[], + certname=None, + dry_run=False, + register_unsafely_without_email=False, + update_registration=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_bootstrap=False, + quiet=False, + staging=False, + debug=False, + debug_challenges=False, + no_verify_ssl=False, + tls_sni_01_port=challenges.TLSSNI01Response.PORT, + tls_sni_01_address="", + http01_port=challenges.HTTP01Response.PORT, + http01_address="", + break_my_certs=False, rsa_key_size=2048, + must_staple=False, + redirect=None, + auto_hsts=False, + hsts=None, + uir=None, + staple=None, + strict_permissions=False, + pref_challs=[], + validate_hooks=True, + directory_hooks=True, + reuse_key=False, + disable_renew_updates=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="/etc/letsencrypt", work_dir="/var/lib/letsencrypt", logs_dir="/var/log/letsencrypt", - no_verify_ssl=False, - http01_port=challenges.HTTP01Response.PORT, - tls_sni_01_port=challenges.TLSSNI01Response.PORT, + 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_cloudxns=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 - auth_cert_path="./cert.pem", - auth_chain_path="./chain.pem", - strict_permissions=False, ) -STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory" +STAGING_URI = "https://acme-staging-v02.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 `.IConfig` attributes.""" @@ -50,13 +143,13 @@ """Defaults for renewer script.""" -ENHANCEMENTS = ["redirect", "http-header", "ocsp-stapling", "spdy"] +ENHANCEMENTS = ["redirect", "ensure-http-header", "ocsp-stapling", "spdy"] """List of possible :class:`certbot.interfaces.IInstaller` enhancements. List of expected options parameters: - redirect: None -- http-header: TODO +- ensure-http-header: name of header (i.e. Strict-Transport-Security) - ocsp-stapling: certificate chain file path - spdy: TODO @@ -71,6 +164,13 @@ ACCOUNTS_DIR = "accounts" """Directory where all accounts are saved.""" +LE_REUSE_SERVERS = { + 'acme-v02.api.letsencrypt.org/directory': 'acme-v01.api.letsencrypt.org/directory', + 'acme-staging-v02.api.letsencrypt.org/directory': + 'acme-staging.api.letsencrypt.org/directory' +} +"""Servers that can reuse accounts from other servers.""" + BACKUP_DIR = "backups" """Directory (relative to `IConfig.work_dir`) where backups are kept.""" @@ -93,5 +193,35 @@ RENEWAL_CONFIGS_DIR = "renewal" """Renewal configs directory, relative to `IConfig.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 IDisplay.""" + +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 `IConfig.config_dir`.""" + +SSL_DHPARAMS_SRC = pkg_resources.resource_filename( + "certbot", "ssl-dhparams.pem") +"""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 `IConfig.config_dir`.""" + +ALL_SSL_DHPARAMS_HASHES = [ + '9ba6429597aeed2d8617a7705b56e96d044f64b07971659382e426675105654b', +] +"""SHA256 hashes of the contents of all versions of SSL_DHPARAMS_SRC""" diff -Nru python-certbot-0.10.2/certbot/crypto_util.py python-certbot-0.28.0/certbot/crypto_util.py --- python-certbot-0.10.2/certbot/crypto_util.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/crypto_util.py 2018-11-07 21:14:56.000000000 +0000 @@ -4,18 +4,28 @@ is capable of handling the signatures. """ +import hashlib import logging import os -import traceback +import warnings -import OpenSSL import pyrfc3339 import six import zope.component +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +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 +# https://github.com/python/typeshed/tree/master/third_party/2/cryptography +from cryptography import x509 # type: ignore +from OpenSSL import crypto +from OpenSSL import SSL # type: ignore from acme import crypto_util as acme_crypto_util -from acme import jose - +from acme.magic_typing import IO # pylint: disable=unused-import, no-name-in-module +from certbot import compat from certbot import errors from certbot import interfaces from certbot import util @@ -46,24 +56,23 @@ try: key_pem = make_key(key_size) except ValueError as err: - logger.exception(err) + logger.error("", exc_info=True) raise err config = zope.component.getUtility(interfaces.IConfig) # Save file - util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), + util.make_or_verify_dir(key_dir, 0o700, compat.os_geteuid(), config.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) - - logger.info("Generating key (%d bits): %s", key_size, key_path) + logger.debug("Generating key (%d bits): %s", key_size, key_path) return util.Key(key_path, key_pem) -def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"): +def init_save_csr(privkey, names, path): """Initialize a CSR with the given private key. :param privkey: Key to include in the CSR @@ -79,61 +88,19 @@ """ config = zope.component.getUtility(interfaces.IConfig) - csr_pem, csr_der = make_csr(privkey.pem, names, - must_staple=config.must_staple) + csr_pem = acme_crypto_util.make_csr( + privkey.pem, names, must_staple=config.must_staple) # Save CSR - util.make_or_verify_dir(path, 0o755, os.geteuid(), + util.make_or_verify_dir(path, 0o755, compat.os_geteuid(), config.strict_permissions) csr_f, csr_filename = util.unique_file( - os.path.join(path, csrname), 0o644, "wb") - csr_f.write(csr_pem) - csr_f.close() - - logger.info("Creating CSR: %s", csr_filename) - - return util.CSR(csr_filename, csr_der, "der") - - -# Lower level functions -def make_csr(key_str, domains, must_staple=False): - """Generate a CSR. - - :param str key_str: PEM-encoded RSA key. - :param list domains: Domains included in the certificate. - - .. todo:: Detect duplicates in `domains`? Using a set doesn't - preserve order... - - :returns: new CSR in PEM and DER form containing all domains - :rtype: tuple + os.path.join(path, "csr-certbot.pem"), 0o644, "wb") + with csr_f: + csr_f.write(csr_pem) + logger.debug("Creating CSR: %s", csr_filename) - """ - assert domains, "Must provide one or more hostnames for the CSR." - pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key_str) - req = OpenSSL.crypto.X509Req() - req.get_subject().CN = domains[0] - # TODO: what to put into req.get_subject()? - # TODO: put SAN if len(domains) > 1 - extensions = [ - OpenSSL.crypto.X509Extension( - b"subjectAltName", - critical=False, - value=", ".join("DNS:%s" % d for d in domains).encode('ascii') - ) - ] - if must_staple: - extensions.append(OpenSSL.crypto.X509Extension( - b"1.3.6.1.5.5.7.1.24", - critical=False, - value=b"DER:30:03:02:01:05")) - req.add_extensions(extensions) - req.set_version(2) - req.set_pubkey(pkey) - req.sign(pkey, "sha256") - return tuple(OpenSSL.crypto.dump_certificate_request(method, req) - for method in (OpenSSL.crypto.FILETYPE_PEM, - OpenSSL.crypto.FILETYPE_ASN1)) + return util.CSR(csr_filename, csr_pem, "pem") # WARNING: the csr and private key file are possible attack vectors for TOCTOU @@ -153,11 +120,11 @@ """ try: - req = OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_PEM, csr) + req = crypto.load_certificate_request( + crypto.FILETYPE_PEM, csr) return req.verify(req.get_pubkey()) - except OpenSSL.crypto.Error as error: - logger.debug(error, exc_info=True) + except crypto.Error: + logger.debug("", exc_info=True) return False @@ -171,13 +138,13 @@ :rtype: bool """ - req = OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_PEM, csr) - pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey) + req = crypto.load_certificate_request( + crypto.FILETYPE_PEM, csr) + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, privkey) try: return req.verify(pkey) - except OpenSSL.crypto.Error as error: - logger.debug(error, exc_info=True) + except crypto.Error: + logger.debug("", exc_info=True) return False @@ -187,22 +154,27 @@ :param str csrfile: CSR filename :param str data: contents of the CSR file - :returns: (`OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, + :returns: (`crypto.FILETYPE_PEM`, util.CSR object representing the CSR, list of domains requested in the CSR) :rtype: tuple """ - for form, typ in (("der", OpenSSL.crypto.FILETYPE_ASN1,), - ("pem", OpenSSL.crypto.FILETYPE_PEM,),): + 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: - domains = get_names_from_csr(data, typ) - except OpenSSL.crypto.Error: - logger.debug("CSR parse error (form=%s, typ=%s):", form, typ) - logger.debug(traceback.format_exc()) - continue - return typ, util.CSR(file=csrfile, data=data, form=form), domains - raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) + 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): @@ -215,9 +187,9 @@ """ assert bits >= 1024 # XXX - key = OpenSSL.crypto.PKey() - key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) - return OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, bits) + return crypto.dump_privatekey(crypto.FILETYPE_PEM, key) def valid_privkey(privkey): @@ -230,12 +202,114 @@ """ try: - return OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, privkey).check() - except (TypeError, OpenSSL.crypto.Error): + return crypto.load_privatekey( + crypto.FILETYPE_PEM, privkey).check() + except (TypeError, crypto.Error): return False +def verify_renewable_cert(renewable_cert): + """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 `.storage.RenewableCert` renewable_cert: cert to verify + + :raises errors.Error: If verification fails. + """ + verify_renewable_cert_sig(renewable_cert) + verify_fullchain(renewable_cert) + verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey) + + +def verify_renewable_cert_sig(renewable_cert): + """ Verifies the signature of a `.storage.RenewableCert` object. + + :param `.storage.RenewableCert` renewable_cert: cert to verify + + :raises errors.Error: If signature verification fails. + """ + try: + with open(renewable_cert.chain, 'rb') as chain_file: # type: IO[bytes] + chain = x509.load_pem_x509_certificate(chain_file.read(), default_backend()) + with open(renewable_cert.cert, 'rb') as cert_file: # type: IO[bytes] + cert = x509.load_pem_x509_certificate(cert_file.read(), default_backend()) + pk = chain.public_key() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + if isinstance(pk, RSAPublicKey): + # https://github.com/python/typeshed/blob/master/third_party/2/cryptography/hazmat/primitives/asymmetric/rsa.pyi + verifier = pk.verifier( # type: ignore + cert.signature, PKCS1v15(), cert.signature_hash_algorithm + ) + verifier.update(cert.tbs_certificate_bytes) + verifier.verify() + elif isinstance(pk, EllipticCurvePublicKey): + verifier = pk.verifier( + cert.signature, ECDSA(cert.signature_hash_algorithm) + ) + verifier.update(cert.tbs_certificate_bytes) + verifier.verify() + else: + raise errors.Error("Unsupported public key type") + except (IOError, ValueError, InvalidSignature) as e: + error_str = "verifying the signature of the cert located at {0} has failed. \ + Details: {1}".format(renewable_cert.cert, e) + logger.exception(error_str) + raise errors.Error(error_str) + + +def verify_cert_matches_priv_key(cert_path, key_path): + """ Verifies that the private key and cert match. + + :param str cert_path: path to a cert in PEM format + :param str key_path: path to a private key file + + :raises errors.Error: If they don't match. + """ + try: + context = SSL.Context(SSL.SSLv23_METHOD) + context.use_certificate_file(cert_path) + context.use_privatekey_file(key_path) + context.check_privatekey() + except (IOError, SSL.Error) as e: + error_str = "verifying the cert located at {0} matches the \ + private key located at {1} has failed. \ + Details: {2}".format(cert_path, + key_path, e) + logger.exception(error_str) + raise errors.Error(error_str) + + +def verify_fullchain(renewable_cert): + """ Verifies that fullchain is indeed cert concatenated with chain. + + :param `.storage.RenewableCert` renewable_cert: cert to verify + + :raises errors.Error: If cert and chain do not combine to fullchain. + """ + try: + with open(renewable_cert.chain) as chain_file: # type: IO[str] + chain = chain_file.read() + with open(renewable_cert.cert) as cert_file: # type: IO[str] + cert = cert_file.read() + with open(renewable_cert.fullchain) as fullchain_file: # type: IO[str] + 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): """Load PEM/DER certificate. @@ -245,118 +319,79 @@ openssl_errors = [] - for file_type in (OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1): + for file_type in (crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1): try: - return OpenSSL.crypto.load_certificate(file_type, data), file_type - except OpenSSL.crypto.Error as error: # TODO: other errors? + 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, load_func, - typ=OpenSSL.crypto.FILETYPE_PEM): + typ=crypto.FILETYPE_PEM): try: return load_func(typ, cert_or_req_str) - except OpenSSL.crypto.Error as error: - logger.exception(error) + except crypto.Error: + logger.error("", exc_info=True) raise def _get_sans_from_cert_or_req(cert_or_req_str, load_func, - typ=OpenSSL.crypto.FILETYPE_PEM): + typ=crypto.FILETYPE_PEM): # 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, typ=OpenSSL.crypto.FILETYPE_PEM): +def get_sans_from_cert(cert, typ=crypto.FILETYPE_PEM): """Get a list of Subject Alternative Names from a certificate. :param str cert: Certificate (encoded). - :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` - - :returns: A list of Subject Alternative Names. - :rtype: list - - """ - return _get_sans_from_cert_or_req( - cert, OpenSSL.crypto.load_certificate, typ) - - -def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): - """Get a list of Subject Alternative Names from a CSR. - - :param str csr: CSR (encoded). - :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` + :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( - csr, OpenSSL.crypto.load_certificate_request, typ) + cert, crypto.load_certificate, typ) def _get_names_from_cert_or_req(cert_or_req, load_func, typ): loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ) - common_name = loaded_cert_or_req.get_subject().CN - # pylint: disable=protected-access - sans = acme_crypto_util._pyopenssl_cert_or_req_san(loaded_cert_or_req) + return _get_names_from_loaded_cert_or_req(loaded_cert_or_req) - if common_name is None: - return sans - else: - return [common_name] + [d for d in sans if d != common_name] +def _get_names_from_loaded_cert_or_req(loaded_cert_or_req): + # pylint: disable=protected-access + return acme_crypto_util._pyopenssl_cert_or_req_all_names(loaded_cert_or_req) -def get_names_from_cert(csr, typ=OpenSSL.crypto.FILETYPE_PEM): + +def get_names_from_cert(csr, typ=crypto.FILETYPE_PEM): """Get a list of domains from a cert, including the CN if it is set. :param str cert: Certificate (encoded). - :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` - - :returns: A list of domain names. - :rtype: list - - """ - return _get_names_from_cert_or_req( - csr, OpenSSL.crypto.load_certificate, typ) - - -def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): - """Get a list of domains from a CSR, including the CN if it is set. - - :param str csr: CSR (encoded). - :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` + :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, OpenSSL.crypto.load_certificate_request, typ) + csr, crypto.load_certificate, typ) -def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): +def dump_pyopenssl_chain(chain, filetype=crypto.FILETYPE_PEM): """Dump certificate chain into a bundle. - :param list chain: List of `OpenSSL.crypto.X509` (or wrapped in - `acme.jose.ComparableX509`). + :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... - - def _dump_cert(cert): - if isinstance(cert, jose.ComparableX509): - # pylint: disable=protected-access - cert = cert.wrapped - return OpenSSL.crypto.dump_certificate(filetype, cert) - - # assumes that OpenSSL.crypto.dump_certificate includes ending - # newline character - return b"".join(_dump_cert(cert) for cert in chain) + return acme_crypto_util.dump_pyopenssl_chain(chain, filetype) def notBefore(cert_path): @@ -368,7 +403,7 @@ :rtype: :class:`datetime.datetime` """ - return _notAfterBefore(cert_path, OpenSSL.crypto.X509.get_notBefore) + return _notAfterBefore(cert_path, crypto.X509.get_notBefore) def notAfter(cert_path): @@ -380,22 +415,23 @@ :rtype: :class:`datetime.datetime` """ - return _notAfterBefore(cert_path, OpenSSL.crypto.X509.get_notAfter) + return _notAfterBefore(cert_path, crypto.X509.get_notAfter) def _notAfterBefore(cert_path, method): """Internal helper function for finding notbefore/notafter. :param str cert_path: path to a cert in PEM format - :param function method: one of ``OpenSSL.crypto.X509.get_notBefore`` - or ``OpenSSL.crypto.X509.get_notAfter`` + :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) as f: - x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) # pyopenssl always returns bytes timestamp = method(x509) @@ -408,3 +444,34 @@ if six.PY3: timestamp_str = timestamp_str.decode('ascii') return pyrfc3339.parse(timestamp_str) + + +def sha256sum(filename): + """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, 'rU') as file_d: + sha256.update(file_d.read().encode('UTF-8')) + return sha256.hexdigest() + +def cert_and_chain_from_fullchain(fullchain_pem): + """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 + + """ + cert = crypto.dump_certificate(crypto.FILETYPE_PEM, + crypto.load_certificate(crypto.FILETYPE_PEM, fullchain_pem)).decode() + chain = fullchain_pem[len(cert):].lstrip() + return (cert, chain) diff -Nru python-certbot-0.10.2/certbot/display/completer.py python-certbot-0.28.0/certbot/display/completer.py --- python-certbot-0.10.2/certbot/display/completer.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/display/completer.py 2018-11-07 21:14:56.000000000 +0000 @@ -4,7 +4,7 @@ try: import readline except ImportError: - import certbot.display.dummy_readline as readline + import certbot.display.dummy_readline as readline # type: ignore class Completer(object): @@ -49,9 +49,9 @@ readline.set_completer(self.complete) readline.set_completer_delims(' \t\n;') - # readline can be implemented using GNU readline or libedit + # readline can be implemented using GNU readline, pyreadline or libedit # which have different configuration syntax - if 'libedit' in readline.__doc__: + 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') diff -Nru python-certbot-0.10.2/certbot/display/enhancements.py python-certbot-0.28.0/certbot/display/enhancements.py --- python-certbot-0.10.2/certbot/display/enhancements.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/display/enhancements.py 2018-11-07 21:14:56.000000000 +0000 @@ -42,12 +42,14 @@ """ choices = [ - ("Easy", "Allow both HTTP and HTTPS access to these sites"), - ("Secure", "Make all requests redirect to secure HTTPS access"), + ("No redirect", "Make no further changes to the webserver configuration."), + ("Redirect", "Make all requests redirect to secure HTTPS access. " + "Choose this for new sites, or if you're confident your site works on HTTPS. " + "You can undo this change by editing your web server's configuration."), ] code, selection = util(interfaces.IDisplay).menu( - "Please choose whether HTTPS access is required or optional.", + "Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.", choices, default=0, cli_flag="--redirect / --no-redirect", force_interactive=True) diff -Nru python-certbot-0.10.2/certbot/display/ops.py python-certbot-0.28.0/certbot/display/ops.py --- python-certbot-0.10.2/certbot/display/ops.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/display/ops.py 2018-11-07 21:14:56.000000000 +0000 @@ -86,13 +86,31 @@ else: return None +def choose_values(values, question=None): + """Display screen to let user pick one or multiple values from the provided + list. -def choose_names(installer): + :param list values: Values to select from + + :returns: List of selected values + :rtype: list + """ + code, items = z_util(interfaces.IDisplay).checklist( + question, tags=values, force_interactive=True) + if code == display_util.OK and items: + return items + else: + return [] + +def choose_names(installer, question=None): """Display screen to select domains to validate. :param installer: An installer object :type installer: :class:`certbot.interfaces.IInstaller` + :param `str` question: Overriding dialog question to ask the user if asked + to choose from domain names. + :returns: List of selected names :rtype: `list` of `str` @@ -108,7 +126,7 @@ return _choose_names_manually( "No names were found in your configuration files. ") - code, names = _filter_names(names) + code, names = _filter_names(names, question) if code == display_util.OK and names: return names else: @@ -142,7 +160,7 @@ return sorted(FQDNs, key=lambda fqdn: fqdn.split('.')[::-1][1:]) -def _filter_names(names): +def _filter_names(names, override_question=None): """Determine which names the user would like to select from a list. :param list names: domain names @@ -155,10 +173,12 @@ """ #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?" code, names = z_util(interfaces.IDisplay).checklist( - "Which names would you like to activate HTTPS for?", - tags=sorted_names, cli_flag="--domains", force_interactive=True) + question, tags=sorted_names, cli_flag="--domains", force_interactive=True) return code, [str(s) for s in names] @@ -192,12 +212,7 @@ try: domain_list[i] = util.enforce_domain_sanity(domain) except errors.ConfigurationError as e: - try: # Python 2 - # pylint: disable=no-member - err_msg = e.message.encode('utf-8') - except AttributeError: - err_msg = str(e) - invalid_domains[domain] = err_msg + invalid_domains[domain] = str(e) if len(invalid_domains): retry_message = ( @@ -294,3 +309,61 @@ domains[-1]) return "" + + +def _get_validated(method, validator, message, default=None, **kwargs): + if default is not None: + try: + validator(default) + except errors.Error as 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) + zope.component.getUtility(interfaces.IDisplay).notification(str(error), pause=False) + else: + return code, raw + + +def validated_input(validator, *args, **kwargs): + """Like `~certbot.interfaces.IDisplay.input`, but with validation. + + :param callable validator: A method which will be called on the + supplied input. If the method raises a `errors.Error`, its + text will be displayed and the user will be re-prompted. + :param list `*args`: Arguments to be passed to `~certbot.interfaces.IDisplay.input`. + :param dict `**kwargs`: Arguments to be passed to `~certbot.interfaces.IDisplay.input`. + :return: as `~certbot.interfaces.IDisplay.input` + :rtype: tuple + """ + return _get_validated(zope.component.getUtility(interfaces.IDisplay).input, + validator, *args, **kwargs) + + +def validated_directory(validator, *args, **kwargs): + """Like `~certbot.interfaces.IDisplay.directory_select`, but with validation. + + :param callable validator: A method which will be called on the + supplied input. If the method raises a `errors.Error`, its + text will be displayed and the user will be re-prompted. + :param list `*args`: Arguments to be passed to `~certbot.interfaces.IDisplay.directory_select`. + :param dict `**kwargs`: Arguments to be passed to + `~certbot.interfaces.IDisplay.directory_select`. + :return: as `~certbot.interfaces.IDisplay.directory_select` + :rtype: tuple + """ + return _get_validated(zope.component.getUtility(interfaces.IDisplay).directory_select, + validator, *args, **kwargs) diff -Nru python-certbot-0.10.2/certbot/display/util.py python-certbot-0.28.0/certbot/display/util.py --- python-certbot-0.10.2/certbot/display/util.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/display/util.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,12 +1,12 @@ """Certbot display.""" import logging import os -import textwrap import sys +import textwrap -import six import zope.interface +from certbot import compat from certbot import constants from certbot import interfaces from certbot import errors @@ -24,11 +24,15 @@ """Display exit code for a user canceling the display.""" HELP = "help" -"""Display exit code when for when the user requests more help.""" +"""Display exit code when for when the user requests more help. (UNUSED)""" ESC = "esc" -"""Display exit code when the user hits Escape""" +"""Display exit code when the user hits Escape (UNUSED)""" +# Display constants +SIDE_FRAME = ("- " * 39) + "-" +"""Display boundary (alternates spaces, so when copy-pasted, markdown doesn't interpret +it as a heading)""" def _wrap_lines(msg): """Format lines nicely to 80 chars. @@ -49,7 +53,38 @@ break_long_words=False, break_on_hyphens=False)) - return os.linesep.join(fixed_l) + return '\n'.join(fixed_l) + + +def input_with_timeout(prompt=None, timeout=36000.0): + """Get user input with a timeout. + + Behaves the same as six.moves.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 six.moves.input based on + # https://github.com/python/cpython/blob/baf7bb30a02aabde260143136bdf5b3738a1d409/Lib/getpass.py#L129 + if prompt: + sys.stdout.write(prompt) + sys.stdout.flush() + + line = compat.readline_with_timeout(timeout, prompt) + + if not line: + raise EOFError + return line.rstrip('\n') + @zope.interface.implementer(interfaces.IDisplay) class FileDisplay(object): @@ -75,20 +110,20 @@ because it won't cause any workflow regressions """ - side_frame = "-" * 79 if wrap: message = _wrap_lines(message) self.outfile.write( "{line}{frame}{line}{msg}{line}{frame}{line}".format( - line=os.linesep, frame=side_frame, msg=message)) + line=os.linesep, frame=SIDE_FRAME, msg=message)) + self.outfile.flush() if pause: if self._can_interact(force_interactive): - six.moves.input("Press Enter to Continue") + input_with_timeout("Press Enter to Continue") else: logger.debug("Not pausing for user confirmation") - def menu(self, message, choices, ok_label="", cancel_label="", - help_label="", default=None, + def menu(self, message, choices, ok_label=None, cancel_label=None, + help_label=None, default=None, cli_flag=None, force_interactive=False, **unused_kwargs): # pylint: disable=unused-argument """Display a menu. @@ -123,7 +158,6 @@ def input(self, message, default=None, cli_flag=None, force_interactive=False, **unused_kwargs): - # pylint: disable=no-self-use """Accept input from the user. :param str message: message to display to the user @@ -141,12 +175,9 @@ if self._return_default(message, default, cli_flag, force_interactive): return OK, default - ans = six.moves.input( - textwrap.fill( - "%s (Enter 'c' to cancel): " % message, - 80, - break_long_words=False, - break_on_hyphens=False)) + # Trailing space must be added outside of _wrap_lines to be preserved + message = _wrap_lines("%s (Enter 'c' to cancel):" % message) + " " + ans = input_with_timeout(message) if ans == "c" or ans == "C": return CANCEL, "-1" @@ -175,15 +206,14 @@ if self._return_default(message, default, cli_flag, force_interactive): return default - side_frame = ("-" * 79) + os.linesep - message = _wrap_lines(message) self.outfile.write("{0}{frame}{msg}{0}{frame}".format( - os.linesep, frame=side_frame, msg=message)) + os.linesep, frame=SIDE_FRAME + os.linesep, msg=message)) + self.outfile.flush() while True: - ans = six.moves.input("{yes}/{no}: ".format( + ans = input_with_timeout("{yes}/{no}: ".format( yes=_parens_around_char(yes_label), no=_parens_around_char(no_label))) @@ -196,14 +226,13 @@ ans.startswith(no_label[0].upper())): return False - def checklist(self, message, tags, default_status=True, default=None, + def checklist(self, message, tags, default=None, cli_flag=None, force_interactive=False, **unused_kwargs): # pylint: disable=unused-argument """Display a checklist. :param str message: Message to display to user :param list tags: `str` tags to select, len(tags) > 0 - :param bool default_status: Not used for FileDisplay :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 @@ -236,6 +265,7 @@ else: self.outfile.write( "** Error - Invalid selection **%s" % os.linesep) + self.outfile.flush() else: return code, [] @@ -352,22 +382,18 @@ # Write out the message to the user self.outfile.write( "{new}{msg}{new}".format(new=os.linesep, msg=message)) - side_frame = ("-" * 79) + os.linesep - self.outfile.write(side_frame) + self.outfile.write(SIDE_FRAME + os.linesep) # Write out the menu choices for i, desc in enumerate(choices, 1): - self.outfile.write( - textwrap.fill( - "{num}: {desc}".format(num=i, desc=desc), - 80, - break_long_words=False, - break_on_hyphens=False)) + msg = "{num}: {desc}".format(num=i, desc=desc) + self.outfile.write(_wrap_lines(msg)) # Keep this outside of the textwrap self.outfile.write(os.linesep) - self.outfile.write(side_frame) + self.outfile.write(SIDE_FRAME + os.linesep) + self.outfile.flush() def _get_valid_int_ans(self, max_): """Get a numerical selection. @@ -389,7 +415,7 @@ input_msg = ("Press 1 [enter] to confirm the selection " "(press 'c' to cancel): ") while selection < 1: - ans = six.moves.input(input_msg) + ans = input_with_timeout(input_msg) if ans.startswith("c") or ans.startswith("C"): return CANCEL, -1 try: @@ -401,6 +427,7 @@ except ValueError: self.outfile.write( "{0}** Invalid input **{0}".format(os.linesep)) + self.outfile.flush() return OK, selection @@ -450,12 +477,12 @@ :param bool wrap: Whether or not the application should wrap text """ - side_frame = "-" * 79 if wrap: message = _wrap_lines(message) self.outfile.write( "{line}{frame}{line}{msg}{line}{frame}{line}".format( - line=os.linesep, frame=side_frame, msg=message)) + line=os.linesep, frame=SIDE_FRAME, msg=message)) + self.outfile.flush() def menu(self, message, choices, ok_label=None, cancel_label=None, help_label=None, default=None, cli_flag=None, **unused_kwargs): diff -Nru python-certbot-0.10.2/certbot/eff.py python-certbot-0.28.0/certbot/eff.py --- python-certbot-0.10.2/certbot/eff.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/eff.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,98 @@ +"""Subscribes users to the EFF newsletter.""" +import logging + +import requests +import zope.component + +from certbot import constants +from certbot import interfaces + + +logger = logging.getLogger(__name__) + + +def handle_subscription(config): + """High level function to take care of EFF newsletter subscriptions. + + The user may be asked if they want to sign up for the newsletter if + they have not already specified. + + :param .IConfig config: Client configuration. + + """ + if config.email is None: + if config.eff_email: + _report_failure("you didn't provide an e-mail address") + return + if config.eff_email is None: + config.eff_email = _want_subscription() + if config.eff_email: + subscribe(config.email) + + +def _want_subscription(): + """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 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. ") + display = zope.component.getUtility(interfaces.IDisplay) + return display.yesno(prompt, default=False) + + +def subscribe(email): + """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.debug('Sending POST request to %s:\n%s', url, data) + _check_response(requests.post(url, data=data)) + + +def _check_response(response): + """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 response.json()['status'] == False: + _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=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.') + reporter = zope.component.getUtility(interfaces.IReporter) + reporter.add_message(''.join(msg), reporter.LOW_PRIORITY) diff -Nru python-certbot-0.10.2/certbot/error_handler.py python-certbot-0.28.0/certbot/error_handler.py --- python-certbot-0.10.2/certbot/error_handler.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/error_handler.py 2018-11-07 21:14:56.000000000 +0000 @@ -5,6 +5,10 @@ import signal import traceback +# pylint: disable=unused-import, no-name-in-module +from acme.magic_typing import Any, Callable, Dict, List, Union +# pylint: enable=unused-import, no-name-in-module + from certbot import errors logger = logging.getLogger(__name__) @@ -24,23 +28,23 @@ if signal.getsignal(signal_code) != signal.SIG_IGN: _SIGNALS.append(signal_code) - class ErrorHandler(object): """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: + 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) + handler = ErrorHandler(cleanup1_func, *cleanup1_args, **cleanup1_kwargs) + handler.register(cleanup2_func, *cleanup2_args, **cleanup2_kwargs) - with handler: - do_something() + with handler: + do_something() - Or for one cleanup function: + Or for one cleanup function:: - with ErrorHandler(func, args, kwargs): - do_something() + 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. @@ -54,10 +58,11 @@ """ def __init__(self, func=None, *args, **kwargs): + self.call_on_regular_exit = False self.body_executed = False - self.funcs = [] - self.prev_handlers = {} - self.received_signals = [] + self.funcs = [] # type: List[Callable[[], Any]] + self.prev_handlers = {} # type: Dict[int, Union[int, None, Callable]] + self.received_signals = [] # type: List[int] if func is not None: self.register(func, *args, **kwargs) @@ -69,8 +74,11 @@ self.body_executed = True retval = False # SystemExit is ignored to properly handle forks that don't exec - if exec_type in (None, SystemExit): + if exec_type is SystemExit: return retval + elif 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 @@ -84,7 +92,8 @@ return retval def register(self, func, *args, **kwargs): - """Sets func to be called with *args and **kwargs during cleanup + # type: (Callable, *Any, **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 @@ -97,9 +106,8 @@ while self.funcs: try: self.funcs[-1]() - except Exception as error: # pylint: disable=broad-except - logger.error("Encountered exception during recovery") - logger.exception(error) + except Exception: # pylint: disable=broad-except + logger.error("Encountered exception during recovery: ", exc_info=True) self.funcs.pop() def _set_signal_handlers(self): @@ -118,9 +126,9 @@ self.prev_handlers.clear() def _signal_handler(self, signum, unused_frame): - """Replacement function for handling recieved signals. + """Replacement function for handling received signals. - Store the recieved signal. If we are executing the code block in + 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 @@ -135,3 +143,15 @@ 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=None, *args, **kwargs): + ErrorHandler.__init__(self, func, *args, **kwargs) + self.call_on_regular_exit = True + diff -Nru python-certbot-0.10.2/certbot/errors.py python-certbot-0.28.0/certbot/errors.py --- python-certbot-0.10.2/certbot/errors.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/errors.py 2018-11-07 21:14:56.000000000 +0000 @@ -30,7 +30,13 @@ class SignalExit(Error): - """A Unix signal was recieved while in the ErrorHandler context manager.""" + """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 @@ -81,6 +87,10 @@ """Certbot Plugin function not supported error.""" +class PluginStorageError(PluginError): + """Certbot Plugin Storage error.""" + + class StandaloneBindError(Error): """Standalone plugin bind error.""" diff -Nru python-certbot-0.10.2/certbot/hooks.py python-certbot-0.28.0/certbot/hooks.py --- python-certbot-0.10.2/certbot/hooks.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/hooks.py 2018-11-07 21:14:56.000000000 +0000 @@ -6,6 +6,7 @@ from subprocess import Popen, PIPE +from acme.magic_typing import Set, List # pylint: disable=unused-import, no-name-in-module from certbot import errors from certbot import util @@ -13,12 +14,15 @@ logger = logging.getLogger(__name__) + def validate_hooks(config): """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): """Extract the program run by a shell command. @@ -44,59 +48,180 @@ cmd = shell_cmd.split(None, 1)[0] if not _prog(cmd): path = os.environ["PATH"] - msg = "Unable to find {2}-hook command {0} in the PATH.\n(PATH is {1})".format( - cmd, path, hook_name) + if os.path.exists(cmd): + msg = "{1}-hook command {0} exists, but is not executable.".format(cmd, hook_name) + else: + msg = "Unable to find {2}-hook command {0} in the PATH.\n(PATH is {1})".format( + cmd, path, hook_name) + raise errors.HookCommandNotFound(msg) + def pre_hook(config): - "Run pre-hook if it's defined and hasn't been run." + """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 and cmd not in pre_hook.already: - logger.info("Running pre-hook command: %s", cmd) - _run_hook(cmd) - pre_hook.already.add(cmd) - elif cmd: - logger.info("Pre-hook command already run, skipping: %s", cmd) + if cmd: + _run_pre_hook_if_necessary(cmd) + -pre_hook.already = set() +executed_pre_hooks = set() # type: Set[str] + + +def _run_pre_hook_if_necessary(command): + """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: + logger.info("Running pre-hook command: %s", command) + _run_hook(command) + executed_pre_hooks.add(command) def post_hook(config): - """Run post hook if defined. + """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 - If the verb is renew, we might have more certs to renew, so we wait until - run_saved_post_hooks() is called. """ cmd = config.post_hook # In the "renew" case, we save these up to run at the end if config.verb == "renew": - if cmd and cmd not in post_hook.eventually: - post_hook.eventually.append(cmd) + 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: logger.info("Running post-hook command: %s", cmd) _run_hook(cmd) -post_hook.eventually = [] + +post_hooks = [] # type: List[str] + + +def _run_eventually(command): + """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(): """Run any post hooks that were saved up in the course of the 'renew' verb""" - for cmd in post_hook.eventually: + for cmd in post_hooks: logger.info("Running post-hook command: %s", cmd) _run_hook(cmd) +def deploy_hook(config, domains, lineage_path): + """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) + + def renew_hook(config, domains, lineage_path): - """Run post-renewal hook if defined.""" + """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) + executed_dir_hooks.add(hook) + if config.renew_hook: - if not config.dry_run: - os.environ["RENEWED_DOMAINS"] = " ".join(domains) - os.environ["RENEWED_LINEAGE"] = lineage_path - logger.info("Running renew-hook command: %s", config.renew_hook) - _run_hook(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: - logger.warning("Dry run: skipping renewal hook command: %s", config.renew_hook) + _run_deploy_hook(config.renew_hook, domains, + lineage_path, config.dry_run) + + +def _run_deploy_hook(command, domains, lineage_path, dry_run): + """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 + + """ + if dry_run: + logger.warning("Dry run: skipping deploy hook command: %s", + command) + return + + os.environ["RENEWED_DOMAINS"] = " ".join(domains) + os.environ["RENEWED_LINEAGE"] = lineage_path + logger.info("Running deploy-hook command: %s", command) + _run_hook(command) def _run_hook(shell_cmd): @@ -118,11 +243,25 @@ cmd = Popen(shell_cmd, shell=True, stdout=PIPE, stderr=PIPE, universal_newlines=True) out, err = cmd.communicate() + base_cmd = os.path.basename(shell_cmd.split(None, 1)[0]) + if out: + logger.info('Output from %s:\n%s', base_cmd, out) if cmd.returncode != 0: logger.error('Hook command "%s" returned error code %d', shell_cmd, cmd.returncode) if err: - base_cmd = os.path.basename(shell_cmd.split(None, 1)[0]) logger.error('Error output from %s:\n%s', base_cmd, err) return (err, out) + +def list_hooks(dir_path): + """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 + + """ + paths = (os.path.join(dir_path, f) for f in os.listdir(dir_path)) + return sorted(path for path in paths if util.is_exe(path)) diff -Nru python-certbot-0.10.2/certbot/interfaces.py python-certbot-0.28.0/certbot/interfaces.py --- python-certbot-0.10.2/certbot/interfaces.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/interfaces.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,16 +1,16 @@ """Certbot client interfaces.""" import abc +import six import zope.interface # pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class # pylint: disable=too-few-public-methods +@six.add_metaclass(abc.ABCMeta) class AccountStorage(object): """Accounts storage interface.""" - __metaclass__ = abc.ABCMeta - @abc.abstractmethod def find_all(self): # pragma: no cover """Find all accounts. @@ -32,7 +32,7 @@ raise NotImplementedError() @abc.abstractmethod - def save(self, account): # pragma: no cover + def save(self, account, client): # pragma: no cover """Save account. :raises .AccountStorageError: if account could not be saved @@ -99,7 +99,7 @@ class IPlugin(zope.interface.Interface): """Certbot plugin.""" - def prepare(): + def prepare(): # type: ignore """Prepare the plugin. Finish up any additional initialization. @@ -118,7 +118,7 @@ """ - def more_info(): + def more_info(): # type: ignore """Human-readable string to help the user. Should describe the steps taken and any relevant info to help the user @@ -201,7 +201,9 @@ """ server = zope.interface.Attribute("ACME Directory Resource URI.") email = zope.interface.Attribute( - "Email used for registration and recovery contact. (default: Ask)") + "Email used for registration and recovery contact. Use comma to " + "register multiple emails, ex: u1@example.com,u2@example.com. " + "(default: Ask).") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") must_staple = zope.interface.Attribute( "Adds the OCSP Must Staple extension to the certificate. " @@ -229,12 +231,36 @@ "Port used during tls-sni-01 challenge. " "This only affects the port Certbot listens on. " "A conforming ACME server will still attempt to connect on port 443.") + tls_sni_01_address = zope.interface.Attribute( + "The address the server listens to during tls-sni-01 challenge.") http01_port = zope.interface.Attribute( "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.") + http01_address = zope.interface.Attribute( + "The address the server listens to during http-01 challenge.") + + pref_challs = zope.interface.Attribute( + "Sorted user specified preferred challenges" + "type strings with the most preferred challenge listed first") + + allow_subset_of_names = zope.interface.Attribute( + "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 is a boolean") + + strict_permissions = zope.interface.Attribute( + "Require that all configuration files are owned by the current " + "user; only needed if your config is somewhere unsafe like /tmp/." + "This is a boolean") + + disable_renew_updates = zope.interface.Attribute( + "If updates provided by installer enhancements when Certbot is being run" + " with \"renew\" verb should be disabled.") class IInstaller(IPlugin): """Generic Certbot Installer Interface. @@ -251,7 +277,7 @@ """ - def get_all_names(): + def get_all_names(): # type: ignore """Returns all names that may be authenticated. :rtype: `collections.Iterable` of `str` @@ -288,7 +314,7 @@ """ - def supported_enhancements(): + def supported_enhancements(): # type: ignore """Returns a `collections.Iterable` of supported enhancements. :returns: supported enhancements which should be a subset of @@ -326,7 +352,7 @@ """ - def recovery_routine(): + def recovery_routine(): # type: ignore """Revert configuration to most recent finalized checkpoint. Remove all changes (temporary and permanent) that have not been @@ -337,21 +363,21 @@ """ - def view_config_changes(): + def view_config_changes(): # type: ignore """Display all of the LE config changes. :raises .PluginError: when config changes cannot be parsed """ - def config_test(): + def config_test(): # type: ignore """Make sure the configuration is valid. :raises .MisconfigurationError: when the config is not in a usable state """ - def restart(): + def restart(): # type: ignore """Restart or refresh the server content. :raises .PluginError: when server cannot be restarted @@ -376,8 +402,8 @@ """ - def menu(message, choices, ok_label="OK", - cancel_label="Cancel", help_label="", + def menu(message, choices, ok_label=None, + cancel_label=None, help_label=None, default=None, cli_flag=None, force_interactive=False): """Displays a generic menu. @@ -389,9 +415,9 @@ :param choices: choices :type choices: :class:`list` of :func:`tuple` or :class:`str` - :param str ok_label: label for OK button - :param str cancel_label: label for Cancel button - :param str help_label: label for Help button + :param str ok_label: label for OK button (UNUSED) + :param str cancel_label: label for Cancel button (UNUSED) + :param str help_label: label for Help button (UNUSED) :param int default: default (non-interactive) choice from the menu :param str cli_flag: to automate choice from the menu, eg "--keep" :param bool force_interactive: True if it's safe to prompt the user @@ -450,8 +476,7 @@ """ - def checklist(message, tags, default_state, - default=None, cli_args=None, force_interactive=False): + def checklist(message, tags, default=None, cli_args=None, force_interactive=False): """Allow for multiple selections from a menu. When not setting force_interactive=True, you must provide a @@ -459,7 +484,6 @@ :param str message: message to display to the user :param list tags: where each is of type :class:`str` len(tags) > 0 - :param bool default_status: If True, items are in a selected state by default. :param str default: default (non-interactive) state of the checklist :param str cli_flag: to automate choice from the menu, eg "--domains" :param bool force_interactive: True if it's safe to prompt the user @@ -573,3 +597,75 @@ def print_messages(self): """Prints messages to the user and clears the message queue.""" + + +# 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. + +@six.add_metaclass(abc.ABCMeta) +class GenericUpdater(object): + """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.IInstaller` such as prepare() and restart() + """ + + @abc.abstractmethod + def generic_updates(self, lineage, *args, **kwargs): + """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: storage.RenewableCert + + """ + + +@six.add_metaclass(abc.ABCMeta) +class RenewDeployer(object): + """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. + """ + + @abc.abstractmethod + def renew_deploy(self, lineage, *args, **kwargs): + """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 certficate 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: storage.RenewableCert + + """ diff -Nru python-certbot-0.10.2/certbot/lock.py python-certbot-0.28.0/certbot/lock.py --- python-certbot-0.10.2/certbot/lock.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/lock.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,124 @@ +"""Implements file locks for locking files and directories in UNIX.""" +import errno +import logging +import os + +from certbot import compat +from certbot import errors + +logger = logging.getLogger(__name__) + + +def lock_dir(dir_path): + """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')) + + +class LockFile(object): + """A UNIX lock file. + + 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 __init__(self, path): + """Initialize and acquire the lock file. + + :param str path: path to the file to lock + + :raises errors.LockError: if unable to acquire the lock + + """ + super(LockFile, self).__init__() + self._path = path + self._fd = None + + self.acquire() + + def acquire(self): + """Acquire the lock file. + + :raises errors.LockError: if lock is already held + :raises OSError: if unable to open or stat the lock file + + """ + while self._fd is None: + # Open the file + fd = os.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): + """Try to acquire the lock file without blocking. + + :param int fd: file descriptor of the opened file to lock + + """ + try: + compat.lock_file(fd) + 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): + """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 + + """ + try: + stat1 = os.stat(self._path) + except OSError as err: + if err.errno == errno.ENOENT: + return False + raise + + stat2 = os.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 __repr__(self): + repr_str = '{0}({1}) <'.format(self.__class__.__name__, self._path) + if self._fd is None: + repr_str += 'released>' + else: + repr_str += 'acquired>' + return repr_str + + def release(self): + """Remove, close, and release the lock file.""" + try: + compat.release_locked_file(self._fd, self._path) + finally: + self._fd = None diff -Nru python-certbot-0.10.2/certbot/log.py python-certbot-0.28.0/certbot/log.py --- python-certbot-0.10.2/certbot/log.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/log.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,353 @@ +"""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. + +""" +from __future__ import print_function +import functools +import logging +import logging.handlers +import os +import sys +import tempfile +import traceback + +from acme import messages + +from certbot import compat +from certbot import constants +from certbot import errors +from certbot import util + +# 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(): + """Setup logging before command line arguments are parsed. + + Terminal logging is setup using + `certbot.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)) + stream_handler.setLevel(constants.QUIET_LOGGING_LEVEL) + + 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, log_path=temp_handler.path) + + +def post_arg_parse_setup(config): + """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.interface.IConfig config: Configuration object + + """ + file_handler, file_path = setup_log_file_handler( + config, 'letsencrypt.log', FILE_FMT) + logs_dir = os.path.dirname(file_path) + + 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 = memory_handler.target + memory_handler.setTarget(file_handler) + memory_handler.flush(force=True) + memory_handler.close() + temp_handler.close() + + if config.quiet: + level = constants.QUIET_LOGGING_LEVEL + else: + level = -config.verbose_count * 10 + stderr_handler.setLevel(level) + logger.debug('Root logging level set at %d', level) + logger.info('Saving debug log to %s', file_path) + + sys.excepthook = functools.partial( + post_arg_parse_except_hook, debug=config.debug, log_path=logs_dir) + + +def setup_log_file_handler(config, logfile, fmt): + """Setup file debug logging. + + :param certbot.interface.IConfig 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, compat.os_geteuid(), 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=None): + super(ColoredStreamHandler, self).__init__(stream) + self.colored = (sys.stderr.isatty() if stream is None else + stream.isatty()) + self.red_level = logging.WARNING + + def format(self, record): + """Formats the string representation of record. + + :param logging.LogRecord record: Record to be formatted + + :returns: Formatted, string representation of record + :rtype: str + + """ + out = super(ColoredStreamHandler, self).format(record) + if self.colored and record.levelno >= self.red_level: + return ''.join((util.ANSI_SGR_RED, out, util.ANSI_SGR_RESET)) + else: + 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=None, capacity=10000): + # capacity doesn't matter because should_flush() is overridden + super(MemoryHandler, self).__init__(capacity, target=target) + + def close(self): + """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 = self.target + super(MemoryHandler, self).close() + self.target = target + + def flush(self, force=False): # 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(MemoryHandler, self).flush() + + def shouldFlush(self, record): + """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): + stream = tempfile.NamedTemporaryFile('w', delete=False) + super(TempHandler, self).__init__(stream) + self.path = stream.name + self._delete = True + + def emit(self, record): + """Log the specified logging record. + + :param logging.LogRecord record: Record to be formatted + + """ + self._delete = False + super(TempHandler, self).emit(record) + + def close(self): + """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: + os.remove(self.path) + self._delete = False + super(TempHandler, self).close() + finally: + self.release() + + +def pre_arg_parse_except_hook(memory_handler, *args, **kwargs): + """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, exc_value, trace, debug, log_path): + """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 str log_path: path to file or directory containing the log + + """ + exc_info = (exc_type, exc_value, trace) + # 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 + logger.error('Exiting abnormally:', exc_info=exc_info) + else: + logger.debug('Exiting abnormally:', exc_info=exc_info) + if issubclass(exc_type, errors.Error): + sys.exit(exc_value) + logger.error('An unexpected error occurred:') + if messages.is_acme_error(exc_value): + # Remove the ACME error prefix from the exception + _, _, exc_str = str(exc_value).partition(':: ') + logger.error(exc_str) + else: + traceback.print_exception(exc_type, exc_value, None) + exit_with_log_path(log_path) + + +def exit_with_log_path(log_path): + """Print a message about the log location 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 = 'Please see the ' + if os.path.isdir(log_path): + msg += 'logfiles in {0} '.format(log_path) + else: + msg += "logfile '{0}' ".format(log_path) + msg += 'for more details.' + sys.exit(msg) diff -Nru python-certbot-0.10.2/certbot/main.py python-certbot-0.28.0/certbot/main.py --- python-certbot-0.10.2/certbot/main.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/main.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,45 +1,43 @@ """Certbot main entry point.""" +# pylint: disable=too-many-lines from __future__ import print_function -import atexit import functools import logging.handlers import os import sys -import time -import traceback +import configobj +import josepy as jose import zope.component -from acme import jose -from acme import messages from acme import errors as acme_errors +from acme.magic_typing import Union # pylint: disable=unused-import, no-name-in-module import certbot from certbot import account from certbot import cert_manager -from certbot import client from certbot import cli -from certbot import crypto_util -from certbot import colored_logging +from certbot import client +from certbot import compat from certbot import configuration from certbot import constants +from certbot import crypto_util +from certbot import eff from certbot import errors from certbot import hooks from certbot import interfaces -from certbot import util -from certbot import reporter +from certbot import log from certbot import renewal +from certbot import reporter +from certbot import storage +from certbot import updater +from certbot import util from certbot.display import util as display_util, ops as display_ops from certbot.plugins import disco as plugins_disco from certbot.plugins import selection as plug_sel - - -_PERM_ERR_FMT = os.linesep.join(( - "The following error was encountered:", "{0}", - "If running as non-root, set --config-dir, " - "--logs-dir, and --work-dir to writeable paths.")) +from certbot.plugins import enhancements USER_CANCELLED = ("User chose to cancel the operation and may " "reinvoke the client.") @@ -48,82 +46,104 @@ logger = logging.getLogger(__name__) -def _suggest_donation_if_appropriate(config, action): - """Potentially suggest a donation to support Certbot.""" - if config.staging or config.verb == "renew": +def _suggest_donation_if_appropriate(config): + """Potentially suggest a donation to support Certbot. + + :param config: Configuration object + :type config: interfaces.IConfig + + :returns: `None` + :rtype: None + + """ + assert config.verb != "renew" + if config.staging: # --dry-run implies --staging return - if action not in ["renew", "newcert"]: - return reporter_util = zope.component.getUtility(interfaces.IReporter) msg = ("If you like Certbot, please consider supporting our work by:\n\n" "Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n" "Donating to EFF: https://eff.org/donate-le\n\n") reporter_util.add_message(msg, reporter_util.LOW_PRIORITY) +def _report_successful_dry_run(config): + """Reports on successful dry run + :param config: Configuration object + :type config: interfaces.IConfig -def _report_successful_dry_run(config): + :returns: `None` + :rtype: None + + """ reporter_util = zope.component.getUtility(interfaces.IReporter) - if config.verb != "renew": - reporter_util.add_message("The dry run was successful.", - reporter_util.HIGH_PRIORITY, on_crash=False) + assert config.verb != "renew" + reporter_util.add_message("The dry run was successful.", + reporter_util.HIGH_PRIORITY, on_crash=False) -def _auth_from_available(le_client, config, domains=None, certname=None, lineage=None): +def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=None): """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. - :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname - action can be: "newcert" | "renew" | "reinstall" - """ - # If lineage is specified, use that one instead of looking around for - # a matching one. - if lineage is None: - # This will find a relevant matching lineage that exists - action, lineage = _find_lineage_for_domains_and_certname(config, domains, certname) - else: - # Renewal, where we already know the specific lineage we're - # interested in - action = "renew" + :param config: Configuration object + :type config: interfaces.IConfig - if action == "reinstall": - # The lineage already exists; allow the caller to try installing - # it without getting a new certificate at all. - logger.info("Keeping the existing certificate") - return "reinstall", lineage + :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) try: - if action == "renew": + if lineage is not None: + # Renewal, where we already know the specific lineage we're + # interested in logger.info("Renewing an existing certificate") renewal.renew_cert(config, domains, le_client, lineage) - elif action == "newcert": + else: # TREAT AS NEW REQUEST + assert domains is not None logger.info("Obtaining a new certificate") lineage = le_client.obtain_and_enroll_certificate(domains, certname) if lineage is False: raise errors.Error("Certificate could not be obtained") + elif lineage is not None: + hooks.deploy_hook(config, lineage.names(), lineage.live_dir) finally: hooks.post_hook(config) - if not config.dry_run and not config.verb == "renew": - _report_new_cert(config, lineage.cert, lineage.fullchain) - - return action, lineage + return lineage def _handle_subset_cert_request(config, domains, cert): """Figure out what to do if a previous cert had a subset of the names now requested - :param storage.RenewableCert cert: + :param config: Configuration object + :type config: interfaces.IConfig + + :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 + :rtype: `tuple` of `str` """ existing = ", ".join(cert.names()) @@ -160,11 +180,15 @@ def _handle_identical_cert_request(config, lineage): """Figure out what to do if a lineage has the same names as a previously obtained one - :param storage.RenewableCert lineage: + :param config: Configuration object + :type config: interfaces.IConfig + + :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 + :rtype: `tuple` of `str` """ if not lineage.ensure_deployed(): @@ -189,14 +213,13 @@ "Renew & replace the cert (limit ~5 per 7 days)"] display = zope.component.getUtility(interfaces.IDisplay) - response = display.menu(question, choices, "OK", "Cancel", + response = display.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( - "User chose to cancel the operation and may " - "reinvoke the client.") + "Operation canceled. You may re-run the client.") elif response[1] == 0: return "reinstall", lineage elif response[1] == 1: @@ -210,11 +233,18 @@ the client run if the user chooses to cancel the operation when prompted). + :param config: Configuration object + :type config: interfaces.IConfig + + :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. + a RenewableCert instance or `None` if renewal shouldn't occur. + :rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None` - :raises .Error: If the user would like to rerun the client again. + :raises errors.Error: If the user would like to rerun the client again. """ # Considering the possibility that the requested certificate is @@ -235,14 +265,48 @@ elif subset_names_cert is not None: return _handle_subset_cert_request(config, domains, subset_names_cert) +def _find_cert(config, domains, certname): + """Finds an existing certificate object given domains and/or a certificate name. + + :param config: Configuration object + :type config: interfaces.IConfig + + :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, domains, certname): """Find appropriate lineage based on given domains and/or certname. + :param config: Configuration object + :type config: interfaces.IConfig + + :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 shouldn't occur. + a RenewableCert instance or None if renewal should not occur. + + :rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None` - :raises .Error: If the user would like to rerun the client again. + :raises errors.Error: If the user would like to rerun the client again. """ if not certname: @@ -262,26 +326,85 @@ return "newcert", None else: raise errors.ConfigurationError("No certificate with name {0} found. " - "Use -d to specify domains, or run certbot --certificates to see " + "Use -d to specify domains, or run certbot certificates to see " "possible certificate names.".format(certname)) +def _get_added_removed(after, before): + """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, strings): + """Format list with given character + """ + if len(strings) == 0: + 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, new_domains, certname, old_domains): """Ask user to confirm update cert certname to contain new_domains. + + :param config: Configuration object + :type config: interfaces.IConfig + + :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 - msg = ("Confirm that you intend to update certificate {0} " - "to include domains {1}. Note that it previously " - "contained domains {2}.".format( + + 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, - new_domains, - old_domains)) + _format_list("+", added), + _format_list("-", removed), + br=os.linesep)) obj = zope.component.getUtility(interfaces.IDisplay) if not obj.yesno(msg, "Update cert", "Cancel", default=True): raise errors.ConfigurationError("Specified mismatched cert name and domains.") -def _find_domains_or_certname(config, installer): +def _find_domains_or_certname(config, installer, question=None): """Retrieve domains and certname from config or user input. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param installer: Installer object + :type installer: interfaces.IInstaller + + :param `str` question: Overriding dialog question to ask the user if asked + to choose from domain names. + + :returns: Two-part tuple of domains and certname + :rtype: `tuple` of list of `str` and `str` + + :raises errors.Error: Usage message, if parameters are not used correctly + """ domains = None certname = config.certname @@ -296,7 +419,7 @@ # 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) + domains = display_ops.choose_names(installer, question) if not domains and not certname: raise errors.Error("Please specify --domains, or --installer that " @@ -306,53 +429,79 @@ return domains, certname -def _report_new_cert(config, cert_path, fullchain_path): +def _report_new_cert(config, cert_path, fullchain_path, key_path=None): """Reports the creation of a new certificate to the user. - :param str cert_path: path to cert - :param str fullchain_path: path to full chain + :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: + _report_successful_dry_run(config) + return + + assert cert_path and fullchain_path, "No certificates saved to report." + expiry = crypto_util.notAfter(cert_path).date() reporter_util = zope.component.getUtility(interfaces.IReporter) - if fullchain_path: - # Print the path to fullchain.pem because that's what modern webservers - # (Nginx and Apache2.4) will want. - and_chain = "and chain have" - path = fullchain_path - else: - # Unless we're in .csr mode and there really isn't one - and_chain = "has " - path = cert_path + # Print the path to fullchain.pem because that's what modern webservers + # (Nginx and Apache2.4) will want. verbswitch = ' with the "certonly" option' if config.verb == "run" else "" + privkey_statement = 'Your key file has been saved at:{br}{0}{br}'.format( + key_path, br=os.linesep) if key_path else "" # XXX Perhaps one day we could detect the presence of known old webservers # and say something more informative here. - msg = ('Congratulations! Your certificate {0} been saved at {1}.' - ' Your cert will expire on {2}. To obtain a new or tweaked version of this ' + msg = ('Congratulations! Your certificate and chain have been saved at:{br}' + '{0}{br}{1}' + 'Your cert will expire on {2}. To obtain a new or tweaked version of this ' 'certificate in the future, simply run {3} again{4}. ' 'To non-interactively renew *all* of your certificates, run "{3} renew"' - .format(and_chain, path, expiry, cli.cli_command, verbswitch)) + .format(fullchain_path, privkey_statement, expiry, cli.cli_command, verbswitch, + br=os.linesep)) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) def _determine_account(config): """Determine which account to use. - In order to make the renewer (configuration de/serialization) happy, - if ``config.account`` is ``None``, it will be updated based on the + If ``config.account`` is ``None``, it will be updated based on the user input. Same for ``config.email``. - :param argparse.Namespace config: CLI arguments - :param certbot.interface.IConfig config: Configuration object - :param .AccountStorage account_storage: Account storage. + :param config: Configuration object + :type config: interfaces.IConfig :returns: Account and optionally ACME client API (biproduct of new registration). - :rtype: `tuple` of `certbot.account.Account` and - `acme.client.Client` + :rtype: tuple of :class:`certbot.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): + if config.tos: + return True + msg = ("Please read the Terms of Service at {0}. You " + "must agree in order to register with the ACME " + "server at {1}".format( + terms_of_service, config.server)) + obj = zope.component.getUtility(interfaces.IDisplay) + result = obj.yesno(msg, "Agree", "Cancel", + 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 = None @@ -366,34 +515,90 @@ acc = accounts[0] else: # no account registered yet if config.email is None and not config.register_unsafely_without_email: - config.namespace.email = display_ops.get_email() - - def _tos_cb(regr): - if config.tos: - return True - msg = ("Please read the Terms of Service at {0}. You " - "must agree in order to register with the ACME " - "server at {1}".format( - regr.terms_of_service, config.server)) - obj = zope.component.getUtility(interfaces.IDisplay) - return obj.yesno(msg, "Agree", "Cancel", - cli_flag="--agree-tos", force_interactive=True) - + config.email = display_ops.get_email() try: acc, acme = client.register( config, account_storage, tos_cb=_tos_cb) except errors.MissingCommandlineFlag: raise - except errors.Error as error: - logger.debug(error, exc_info=True) + except errors.Error: + logger.debug("", exc_info=True) raise errors.Error( "Unable to register an account with ACME server") - config.namespace.account = acc.id + config.account = acc.id return acc, acme +def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-branches + """Does the user want to delete their now-revoked certs? If run in non-interactive mode, + deleting happens automatically. + + :param config: parsed command line arguments + :type config: interfaces.IConfig + + :returns: `None` + :rtype: None + + :raises errors.Error: If anything goes wrong, including bad user input, if an overlapping + archive dir is found for the specified lineage, etc ... + """ + display = zope.component.getUtility(interfaces.IDisplay) + reporter_util = zope.component.getUtility(interfaces.IReporter) + + attempt_deletion = config.delete_after_revoke + if attempt_deletion is None: + msg = ("Would you like to delete the cert(s) you just revoked?") + attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No", + force_interactive=True, default=True) + + if not attempt_deletion: + reporter_util.add_message("Not deleting revoked certs.", reporter_util.LOW_PRIORITY) + return + + # config.cert_path must have been set + # config.certname may have been set + assert config.cert_path + + if not config.certname: + config.certname = cert_manager.cert_path_to_lineage(config) + + # don't delete if the archive_dir is used by some other lineage + archive_dir = storage.full_archive_path( + configobj.ConfigObj(storage.renewal_file_for_certname(config, config.certname)), + config, config.certname) + try: + cert_manager.match_and_check_overlaps(config, [lambda x: archive_dir], + lambda x: x.archive_dir, lambda x: x) + except errors.OverlappingMatchFound: + msg = ('Not deleting revoked certs due to overlapping archive dirs. More than ' + 'one lineage is using {0}'.format(archive_dir)) + reporter_util.add_message(''.join(msg), reporter_util.MEDIUM_PRIORITY) + return + except Exception as e: + msg = ('config.default_archive_dir: {0}, config.live_dir: {1}, archive_dir: {2},' + 'original exception: {3}') + msg = msg.format(config.default_archive_dir, config.live_dir, archive_dir, e) + raise errors.Error(msg) + + cert_manager.delete(config) + + def _init_le_client(config, authenticator, installer): + """Initialize Let's Encrypt Client + + :param config: Configuration object + :type config: interfaces.IConfig + + :param authenticator: Acme authentication handler + :type authenticator: interfaces.IAuthenticator + :param installer: Installer object + :type installer: interfaces.IInstaller + + :returns: client: Client object + :rtype: client.Client + + """ if authenticator is not None: # if authenticator was given, then we will need account... acc, acme = _determine_account(config) @@ -406,13 +611,65 @@ return client.Client(config, acc, authenticator, installer, acme=acme) +def unregister(config, unused_plugins): + """Deactivate account on server + + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` + :rtype: None + + """ + account_storage = account.AccountFileStorage(config) + accounts = account_storage.find_all() + reporter_util = zope.component.getUtility(interfaces.IReporter) + + if not accounts: + return "Could not find existing account to deactivate." + yesno = zope.component.getUtility(interfaces.IDisplay).yesno + prompt = ("Are you sure you would like to irrevocably deactivate " + "your account?") + wants_deactivate = 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) + + # delete on boulder + cb_client.acme.deactivate_registration(acc.regr) + account_files = account.AccountFileStorage(config) + # delete local account files + account_files.delete(config.account) + + reporter_util.add_message("Account deactivated.", reporter_util.MEDIUM_PRIORITY) + + def register(config, unused_plugins): - """Create or modify accounts on the server.""" + """Create or modify accounts on the server. + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` or a string indicating and 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() + reporter_util = zope.component.getUtility(interfaces.IReporter) + add_msg = lambda m: reporter_util.add_message(m, reporter_util.MEDIUM_PRIORITY) # registering a new account if not config.update_registration: @@ -435,21 +692,62 @@ return ("--register-unsafely-without-email provided, however, a " "new e-mail address must\ncurrently be provided when " "updating a registration.") - config.namespace.email = display_ops.get_email(optional=False) + config.email = display_ops.get_email(optional=False) acc, acme = _determine_account(config) - acme_client = client.Client(config, acc, None, None, acme=acme) + cb_client = client.Client(config, acc, None, None, acme=acme) # We rely on an exception to interrupt this process if it didn't work. - acc.regr = acme_client.acme.update_registration(acc.regr.update( - body=acc.regr.body.update(contact=('mailto:' + config.email,)))) - account_storage.save_regr(acc) - reporter_util = zope.component.getUtility(interfaces.IReporter) - msg = "Your e-mail address was updated to {0}.".format(config.email) - reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) + acc_contacts = ['mailto:' + email for email in config.email.split(',')] + 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.save_regr(acc, cb_client.acme) + eff.handle_subscription(config) + add_msg("Your e-mail address was updated to {0}.".format(config.email)) + +def _install_cert(config, le_client, domains, lineage=None): + """Install a cert + + :param config: Configuration object + :type config: interfaces.IConfig + + :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 = 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, plugins): - """Install a previously obtained cert in a server.""" + """Install a previously obtained cert in a server. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: List of plugins + :type plugins: `list` of `str` + + :returns: `None` + :rtype: None + + """ # XXX: Update for renewer/RenewableCert # FIXME: be consistent about whether errors are raised or returned from # this function ... @@ -457,27 +755,90 @@ try: installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "install") except errors.PluginSelectionError as e: - return e.message + return str(e) - domains, _ = _find_domains_or_certname(config, installer) - le_client = _init_le_client(config, authenticator=None, installer=installer) - assert config.cert_path is not None # required=True in the subparser - le_client.deploy_certificate( - domains, config.key_path, config.cert_path, config.chain_path, - config.fullchain_path) - le_client.enhance_config(domains, config.chain_path) + 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) + +def _populate_from_certname(config): + """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.namespace.key_path = lineage.key_path + if not config.cert_path: + config.namespace.cert_path = lineage.cert_path + if not config.chain_path: + config.namespace.chain_path = lineage.chain_path + if not config.fullchain_path: + config.namespace.fullchain_path = lineage.fullchain_path + return config + +def _check_certificate_and_key(config): + if not os.path.isfile(os.path.realpath(config.cert_path)): + raise errors.ConfigurationError("Error while reading certificate from path " + "{0}".format(config.cert_path)) + if not os.path.isfile(os.path.realpath(config.key_path)): + raise errors.ConfigurationError("Error while reading private key from path " + "{0}".format(config.key_path)) +def plugins_cmd(config, plugins): + """List server software plugins. + + :param config: Configuration object + :type config: interfaces.IConfig + :param plugins: List of plugins + :type plugins: `list` of `str` -def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print - """List server software plugins.""" + :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(zope.component.getUtility( + interfaces.IDisplay).notification, pause=False) if not config.init and not config.prepare: - print(str(filtered)) + notify(str(filtered)) return filtered.init(config) @@ -485,17 +846,84 @@ logger.debug("Verified plugins: %r", verified) if not config.prepare: - print(str(verified)) + notify(str(verified)) return verified.prepare() available = verified.available() logger.debug("Prepared plugins: %s", available) - print(str(available)) + notify(str(available)) + +def enhance(config, plugins): + """Add security enhancements to existing configuration + + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: List of plugins + :type plugins: `list` of `str` + + :returns: `None` + :rtype: None + + """ + supported_enhancements = ["hsts", "redirect", "uir", "staple"] + # Check that at least one enhancement was requested on command line + oldstyle_enh = any([getattr(config, enh) for enh in supported_enhancements]) + if not enhancements.are_requested(config) and not oldstyle_enh: + msg = ("Please specify one or more enhancement types to configure. To list " + "the available enhancement types, run:\n\n%s --help enhance\n") + logger.warning(msg, sys.argv[0]) + raise errors.MisconfigurationError("No enhancements requested, exiting.") + + try: + installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "enhance") + except errors.PluginSelectionError as e: + return str(e) + + if not enhancements.are_supported(config, installer): + raise errors.NotSupportedError("One ore more of the requested enhancements " + "are not supported by the selected installer") + + certname_question = ("Which certificate would you like to use to enhance " + "your configuration?") + config.certname = cert_manager.get_certnames( + config, "enhance", allow_multiple=False, + custom_prompt=certname_question)[0] + cert_domains = cert_manager.domains_for_certname(config, config.certname) + if config.noninteractive_mode: + domains = cert_domains + else: + domain_question = ("Which domain names would you like to enable the " + "selected enhancements for?") + domains = display_ops.choose_values(cert_domains, domain_question) + if not domains: + raise errors.Error("User cancelled the domain selection. No domains " + "defined, exiting.") + + lineage = cert_manager.lineage_for_certname(config, config.certname) + if not config.chain_path: + config.chain_path = lineage.chain_path + if oldstyle_enh: + le_client = _init_le_client(config, authenticator=None, installer=installer) + le_client.enhance_config(domains, config.chain_path, ask_redirect=False) + if enhancements.are_requested(config): + enhancements.enable(lineage, domains, installer, config) def rollback(config, plugins): - """Rollback server configuration changes made during install.""" + """Rollback server configuration changes made during install. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: List of plugins + :type plugins: `list` of `str` + + :returns: `None` + :rtype: None + + """ client.rollback(config.installer, config.checkpoints, config, plugins) @@ -504,6 +932,15 @@ View checkpoints and associated configuration changes. + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` + :rtype: None + """ client.view_config_changes(config, num=config.num) @@ -512,6 +949,16 @@ Use the information in the config file to make symlinks point to the correct archive directory. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` + :rtype: None + """ cert_manager.update_live_symlinks(config) @@ -520,6 +967,16 @@ Use the information in the config file to rename an existing lineage. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` + :rtype: None + """ cert_manager.rename_lineage(config) @@ -528,90 +985,178 @@ Use the information in the config file to delete an existing lineage. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` + :rtype: None + """ cert_manager.delete(config) def certificates(config, unused_plugins): """Display information about certs configured with Certbot + + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` + :rtype: None + """ cert_manager.certificates(config) def revoke(config, unused_plugins): # TODO: coop with renewal config - """Revoke a previously obtained certificate.""" + """Revoke a previously obtained certificate. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` or string indicating error in case of error + :rtype: None or str + + """ # For user-agent construction - config.namespace.installer = config.namespace.authenticator = "None" + config.installer = config.authenticator = None + + if config.cert_path is None and config.certname: + config.cert_path = storage.cert_path_for_cert_name(config, config.certname) + 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 cert key %s", config.cert_path[0], config.key_path[0]) + crypto_util.verify_cert_matches_priv_key(config.cert_path[0], config.key_path[0]) key = jose.JWK.load(config.key_path[1]) + acme = client.acme_from_config_key(config, key) else: # revocation by account key logger.debug("Revoking %s using Account Key", config.cert_path[0]) acc, _ = _determine_account(config) - key = acc.key - acme = client.acme_from_config_key(config, key) + acme = client.acme_from_config_key(config, acc.key, acc.regr) cert = crypto_util.pyopenssl_load_certificate(config.cert_path[1])[0] + logger.debug("Reason code for revocation: %s", config.reason) try: - acme.revoke(jose.ComparableX509(cert)) + acme.revoke(jose.ComparableX509(cert), config.reason) + _delete_if_appropriate(config) except acme_errors.ClientError as e: - return e.message + return str(e) display_ops.success_revocation(config.cert_path[0]) def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals - """Obtain a certificate and install.""" + """Obtain a certificate and install. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: List of plugins + :type plugins: `list` of `str` + + :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 e.message + return str(e) - domains, certname = _find_domains_or_certname(config, installer) + # 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) - action, lineage = _auth_from_available(le_client, config, domains, certname) + 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) - le_client.deploy_certificate( - domains, lineage.privkey, lineage.cert, - lineage.chain, lineage.fullchain) + 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 + _report_new_cert(config, cert_path, fullchain_path, key_path) - le_client.enhance_config(domains, lineage.chain) + _install_cert(config, le_client, domains, new_lineage) - if action in ("newcert", "reinstall",): + 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) - _suggest_donation_if_appropriate(config, action) + _suggest_donation_if_appropriate(config) -def _csr_obtain_cert(config, le_client): +def _csr_get_and_save_cert(config, le_client): """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: interfaces.IConfig + + :param client: Client object + :type client: client.Client + + :returns: `cert_path` and `fullchain_path` as absolute paths to the actual files + :rtype: `tuple` of `str` + """ - csr, typ = config.actual_csr - certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr, typ) + csr, _ = config.actual_csr + 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) - else: - cert_path, _, cert_fullchain = le_client.save_certificate( - certr, chain, config.cert_path, config.chain_path, config.fullchain_path) - _report_new_cert(config, cert_path, cert_fullchain) + return None, None + cert_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, fullchain_path -def obtain_cert(config, plugins, lineage=None): - """Authenticate & obtain cert, but do not install it. +def renew_cert(config, plugins, lineage): + """Renew & save an existing cert. Do not install it. - This implements the 'certonly' subcommand, and is also called from within the - 'renew' command.""" + :param config: Configuration object + :type config: interfaces.IConfig - # SETUP: Select plugins and construct a client instance + :param plugins: List of plugins + :type plugins: `list` of `str` + + :param lineage: Certificate lineage object + :type lineage: storage.RenewableCert + + :returns: `None` + :rtype: None + + :raises errors.PluginSelectionError: MissingCommandlineFlag if supplied parameters do not pass + + """ try: # installers are used in auth mode to determine domain names installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly") @@ -620,183 +1165,128 @@ raise le_client = _init_le_client(config, auth, installer) - # SHOWTIME: Possibly obtain/renew a cert, and set action to renew | newcert | reinstall - if config.csr is None: # the common case - domains, certname = _find_domains_or_certname(config, installer) - action, _ = _auth_from_available(le_client, config, domains, certname, lineage) - else: - assert lineage is None, "Did not expect a CSR with a RenewableCert" - _csr_obtain_cert(config, le_client) - action = "newcert" + renewed_lineage = _get_and_save_cert(le_client, config, lineage=lineage) - # POSTPRODUCTION: Cleanup, deployment & reporting notify = zope.component.getUtility(interfaces.IDisplay).notification - if config.dry_run: - _report_successful_dry_run(config) - elif config.verb == "renew": - if installer is None: - notify("new certificate deployed without reload, fullchain is {0}".format( - lineage.fullchain), pause=False) - else: - # In case of a renewal, reload server to pick up new certificate. - # In principle we could have a configuration option to inhibit this - # from happening. - installer.restart() - notify("new certificate deployed with reload of {0} server; fullchain is {1}".format( - config.installer, lineage.fullchain), pause=False) - elif action == "reinstall" and config.verb == "certonly": - notify("Certificate not yet due for renewal; no action taken.", pause=False) - _suggest_donation_if_appropriate(config, action) + if installer is None: + notify("new certificate deployed without reload, fullchain is {0}".format( + lineage.fullchain), pause=False) + else: + # In case of a renewal, reload server to pick up new certificate. + # In principle we could have a configuration option to inhibit this + # from happening. + # Run deployer + updater.run_renewal_deployer(config, renewed_lineage, installer) + installer.restart() + notify("new certificate deployed with reload of {0} server; fullchain is {1}".format( + config.installer, lineage.fullchain), pause=False) +def certonly(config, plugins): + """Authenticate & obtain cert, but do not install it. -def renew(config, unused_plugins): - """Renew previously-obtained certificates.""" - try: - renewal.handle_renewal_request(config) - finally: - hooks.run_saved_post_hooks() + This implements the 'certonly' subcommand. + :param config: Configuration object + :type config: interfaces.IConfig -def setup_log_file_handler(config, logfile, fmt): - """Setup file debug logging.""" - log_file_path = os.path.join(config.logs_dir, logfile) - try: - handler = logging.handlers.RotatingFileHandler( - log_file_path, maxBytes=2 ** 20, backupCount=1000) - except IOError as error: - raise errors.Error(_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_formatter.converter = time.gmtime # don't use localtime - handler.setFormatter(handler_formatter) - return handler, log_file_path - - -def _cli_log_handler(level, fmt): - handler = colored_logging.StreamHandler() - handler.setFormatter(logging.Formatter(fmt)) - handler.setLevel(level) - return handler - - -def setup_logging(config): - """Sets up logging to logfiles and the terminal. - - :param certbot.interface.IConfig config: Configuration object - - """ - cli_fmt = "%(message)s" - file_fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" - logfile = "letsencrypt.log" - if config.quiet: - level = constants.QUIET_LOGGING_LEVEL - else: - level = -config.verbose_count * 10 - file_handler, log_file_path = setup_log_file_handler( - config, logfile=logfile, fmt=file_fmt) - cli_handler = _cli_log_handler(level, cli_fmt) + :param plugins: List of plugins + :type plugins: `list` of `str` - # TODO: use fileConfig? + :returns: `None` + :rtype: None - root_logger = logging.getLogger() - root_logger.setLevel(logging.DEBUG) # send all records to handlers - root_logger.addHandler(cli_handler) - root_logger.addHandler(file_handler) + :raises errors.Error: If specified plugin could not be used - logger.debug("Root logging level set at %d", level) - logger.info("Saving debug log to %s", log_file_path) + """ + # SETUP: Select plugins and construct a client instance + try: + # installers are used in auth mode to determine domain names + installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly") + except errors.PluginSelectionError as e: + logger.info("Could not choose appropriate plugin: %s", e) + raise + le_client = _init_le_client(config, auth, installer) -def _handle_exception(exc_type, exc_value, trace, config): - """Logs exceptions and reports them to the user. + if config.csr: + cert_path, fullchain_path = _csr_get_and_save_cert(config, le_client) + _report_new_cert(config, cert_path, fullchain_path) + _suggest_donation_if_appropriate(config) + return - Config is used to determine how to display exceptions to the user. In - general, if config.debug is True, then the full exception and traceback is - shown to the user, otherwise it is suppressed. If config itself is None, - then the traceback and exception is attempted to be written to a logfile. - If this is successful, the traceback is suppressed, otherwise it is shown - to the user. sys.exit is always called with a nonzero status. + domains, certname = _find_domains_or_certname(config, installer) + should_get_cert, lineage = _find_cert(config, domains, certname) - """ - tb_str = "".join(traceback.format_exception(exc_type, exc_value, trace)) - logger.debug("Exiting abnormally:%s%s", os.linesep, tb_str) + if not should_get_cert: + notify = zope.component.getUtility(interfaces.IDisplay).notification + notify("Certificate not yet due for renewal; no action taken.", pause=False) + return - if issubclass(exc_type, Exception) and (config is None or not config.debug): - if config is None: - logfile = "certbot.log" - try: - with open(logfile, "w") as logfd: - traceback.print_exception( - exc_type, exc_value, trace, file=logfd) - assert "--debug" not in sys.argv # config is None if this explodes - except: # pylint: disable=bare-except - sys.exit(tb_str) - if "--debug" in sys.argv: - sys.exit(tb_str) + lineage = _get_and_save_cert(le_client, config, domains, certname, lineage) - if issubclass(exc_type, errors.Error): - sys.exit(exc_value) - else: - # Here we're passing a client or ACME error out to the client at the shell - # Tell the user a bit about what happened, without overwhelming - # them with a full traceback - err = traceback.format_exception_only(exc_type, exc_value)[0] - # Typical error from the ACME module: - # acme.messages.Error: urn:ietf:params:acme:error:malformed :: The - # request message was malformed :: Error creating new registration - # :: Validation of contact mailto:none@longrandomstring.biz failed: - # Server failure at resolver - if (messages.is_acme_error(err) and ":: " in err and - config.verbose_count <= cli.flag_default("verbose_count")): - # prune ACME error code, we have a human description - _code, _sep, err = err.partition(":: ") - msg = "An unexpected error occurred:\n" + err + "Please see the " - if config is None: - msg += "logfile '{0}' for more details.".format(logfile) - else: - msg += "logfiles in {0} for more details.".format(config.logs_dir) - sys.exit(msg) - else: - sys.exit(tb_str) + 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) + _suggest_donation_if_appropriate(config) +def renew(config, unused_plugins): + """Renew previously-obtained certificates. -def make_or_verify_core_dir(directory, mode, uid, strict): - """Make sure directory exists with proper permissions. + :param config: Configuration object + :type config: interfaces.IConfig - :param str directory: Path to a directory. - :param int mode: Directory mode. - :param int uid: Directory owner. - :param bool strict: require directory to be owned by current user + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` - :raises .errors.Error: if the directory cannot be made or verified + :returns: `None` + :rtype: None """ try: - util.make_or_verify_dir(directory, mode, uid, strict) - except OSError as error: - raise errors.Error(_PERM_ERR_FMT.format(error)) + renewal.handle_renewal_request(config) + finally: + hooks.run_saved_post_hooks() + def make_or_verify_needed_dirs(config): - """Create or verify existance of config, work, or logs directories""" - make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, - os.geteuid(), config.strict_permissions) - make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, - os.geteuid(), config.strict_permissions) - # TODO: logs might contain sensitive data such as contents of the - # private key! #525 - make_or_verify_core_dir(config.logs_dir, 0o700, - os.geteuid(), config.strict_permissions) + """Create or verify existence of config, work, and hook directories. + + :param config: Configuration object + :type config: interfaces.IConfig + + :returns: `None` + :rtype: None + + """ + util.set_up_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, + compat.os_geteuid(), config.strict_permissions) + util.set_up_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, + compat.os_geteuid(), 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, + uid=compat.os_geteuid(), + strict=config.strict_permissions) def set_displayer(config): - """Set the displayer""" + """Set the displayer + + :param config: Configuration object + :type config: interfaces.IConfig + + :returns: `None` + :rtype: None + + """ if config.quiet: config.noninteractive_mode = True - displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w")) + displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w")) \ + # type: Union[None, display_util.NoninteractiveDisplay, display_util.FileDisplay] elif config.noninteractive_mode: displayer = display_util.NoninteractiveDisplay(sys.stdout) else: @@ -804,47 +1294,48 @@ config.force_interactive) zope.component.provideUtility(displayer) -def _post_logging_setup(config, plugins, cli_args): - """Perform any setup or configuration tasks that require a logger.""" - # This needs logging, but would otherwise be in HelpfulArgumentParser - if config.validate_hooks: - hooks.validate_hooks(config) +def main(cli_args=sys.argv[1:]): + """Command line argument parsing and main script execution. + + :returns: result of requested command + + :raises errors.Error: OS errors triggered by wrong permissions + :raises errors.Error: error if plugin command is not supported + + """ - cli.possible_deprecation_warning(config) + log.pre_arg_parse_setup() + plugins = plugins_disco.PluginsRegistry.find_all() logger.debug("certbot version: %s", certbot.__version__) # 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) - -def main(cli_args=sys.argv[1:]): - """Command line argument parsing and main script execution.""" - sys.excepthook = functools.partial(_handle_exception, config=None) - plugins = plugins_disco.PluginsRegistry.find_all() - # note: arg parser internally handles --help (and exits afterwards) args = cli.prepare_and_parse_args(plugins, cli_args) config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) - make_or_verify_needed_dirs(config) - - # Setup logging ASAP, otherwise "No handlers could be found for - # logger ..." TODO: this should be done before plugins discovery - setup_logging(config) + # On windows, shell without administrative right cannot create symlinks required by certbot. + # So we check the rights before continuing. + compat.raise_for_non_administrative_windows_rights(config.verb) - _post_logging_setup(config, plugins, cli_args) - - sys.excepthook = functools.partial(_handle_exception, config=config) + 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: + raise set_displayer(config) # Reporter report = reporter.Reporter(config) zope.component.provideUtility(report) - atexit.register(report.atexit_print_messages) + util.atexit_register(report.print_messages) return config.func(config, plugins) diff -Nru python-certbot-0.10.2/certbot/ocsp.py python-certbot-0.28.0/certbot/ocsp.py --- python-certbot-0.10.2/certbot/ocsp.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/ocsp.py 2018-11-07 21:14:56.000000000 +0000 @@ -16,7 +16,7 @@ self.broken = False if not util.exe_exists("openssl"): - logging.info("openssl not installed, can't check revocation") + logger.info("openssl not installed, can't check revocation") self.broken = True return @@ -61,7 +61,7 @@ logger.debug("Querying OCSP for %s", cert_path) logger.debug(" ".join(cmd)) try: - output, err = util.run_script(cmd, log=logging.debug) + 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 @@ -80,7 +80,7 @@ try: url, _err = util.run_script( ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"], - log=logging.debug) + log=logger.debug) except errors.SubprocessError: logger.info("Cannot extract OCSP URI from %s", cert_path) return None, None diff -Nru python-certbot-0.10.2/certbot/plugins/common.py python-certbot-0.28.0/certbot/plugins/common.py --- python-certbot-0.10.2/certbot/plugins/common.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/common.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,4 +1,5 @@ """Plugin common functions.""" +import logging import os import re import shutil @@ -8,12 +9,21 @@ import pkg_resources import zope.interface -from acme.jose import util as jose_util +from josepy import util as jose_util +from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module +from certbot import achallenges # pylint: disable=unused-import from certbot import constants +from certbot import crypto_util +from certbot import errors from certbot import interfaces +from certbot import reverter from certbot import util +from certbot.plugins.storage import PluginStorage + +logger = logging.getLogger(__name__) + def option_namespace(name): """ArgumentParser options namespace (prefix of all options).""" @@ -93,7 +103,121 @@ def conf(self, var): """Find a configuration value for variable ``var``.""" return getattr(self.config, self.dest(var)) -# other + + +class Installer(Plugin): + """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, **kwargs): + super(Installer, self).__init__(*args, **kwargs) + self.storage = PluginStorage(self.config, self.name) + self.reverter = reverter.Reverter(self.config) + + def add_to_checkpoint(self, save_files, save_notes, temporary=False): + """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): + """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): + """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): + """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=1): + """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)) + + def view_config_changes(self): + """Show all of the configuration changes that have taken place. + + :raises .errors.PluginError: If there is a problem while processing + the checkpoints directories. + + """ + try: + self.reverter.view_config_changes() + except errors.ReverterError as err: + raise errors.PluginError(str(err)) + + @property + def ssl_dhparams(self): + """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): + """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): + """Copy Certbot's ssl_dhparams file into the system's config dir if required.""" + return install_version_controlled_file( + self.ssl_dhparams, + self.updated_ssl_dhparams_digest, + constants.SSL_DHPARAMS_SRC, + constants.ALL_SSL_DHPARAMS_HASHES) class Addr(object): @@ -131,7 +255,7 @@ """Normalized representation of addr/port tuple """ if self.ipv6: - return (self._normalize_ipv6(self.tup[0]), self.tup[1]) + return (self.get_ipv6_exploded(), self.tup[1]) return self.tup def __eq__(self, other): @@ -195,23 +319,28 @@ return result -class TLSSNI01(object): - """Abstract base for TLS-SNI-01 challenge performers""" +class ChallengePerformer(object): + """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): self.configurator = configurator - self.achalls = [] - self.indices = [] - self.challenge_conf = os.path.join( - configurator.config.config_dir, "le_tls_sni_01_cert_challenge.conf") - # self.completed = 0 + self.achalls = [] # type: List[achallenges.KeyAuthorizationAnnotatedChallenge] + self.indices = [] # type: List[int] def add_chall(self, achall, idx=None): - """Add challenge to TLSSNI01 object to perform at once. + """Store challenge to be performed when perform() is called. :param .KeyAuthorizationAnnotatedChallenge achall: Annotated - TLSSNI01 challenge. - + challenge. :param int idx: index to challenge in a larger array """ @@ -219,6 +348,27 @@ if idx is not None: self.indices.append(idx) + def perform(self): + """Perform all added challenges. + + :returns: challenge respones + :rtype: `list` of `acme.challenges.KeyAuthorizationChallengeResponse` + + + """ + raise NotImplementedError() + + +class TLSSNI01(ChallengePerformer): + # pylint: disable=abstract-method + """Abstract base for TLS-SNI-01 challenge performers""" + + def __init__(self, configurator): + super(TLSSNI01, self).__init__(configurator) + self.challenge_conf = os.path.join( + configurator.config.config_dir, "le_tls_sni_01_cert_challenge.conf") + # self.completed = 0 + def get_cert_path(self, achall): """Returns standardized name for challenge certificate. @@ -237,6 +387,10 @@ return os.path.join(self.configurator.config.work_dir, achall.chall.encode("token") + '.pem') + def get_z_domain(self, achall): + """Returns z_domain (SNI) name for the challenge.""" + return achall.response(achall.account_key).z_domain.decode("utf-8") + def _setup_challenge_cert(self, achall, cert_key=None): """Generate and write out challenge certificate.""" @@ -262,22 +416,72 @@ return response +def install_version_controlled_file(dest_path, digest_path, src_path, all_hashes): + """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(): + with open(digest_path, "w") as f: + f.write(current_hash) + + def _install_current_file(): + 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 + elif 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 setup_ssl_options(config_dir, src, dest): # pragma: no cover - """Move the ssl_options into position and return the path.""" - option_path = os.path.join(config_dir, dest) - shutil.copyfile(src, option_path) - return option_path - - def dir_setup(test_dir, pkg): # pragma: no cover """Setup the directories necessary for the configurator.""" - temp_dir = tempfile.mkdtemp("temp") - config_dir = tempfile.mkdtemp("config") - work_dir = tempfile.mkdtemp("work") + def expanded_tempdir(prefix): + """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 os.path.realpath(tempfile.mkdtemp(prefix)) + + temp_dir = expanded_tempdir("temp") + config_dir = expanded_tempdir("config") + work_dir = expanded_tempdir("work") os.chmod(temp_dir, constants.CONFIG_DIRS_MODE) os.chmod(config_dir, constants.CONFIG_DIRS_MODE) diff -Nru python-certbot-0.10.2/certbot/plugins/common_test.py python-certbot-0.28.0/certbot/plugins/common_test.py --- python-certbot-0.10.2/certbot/plugins/common_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/common_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,17 +1,34 @@ """Tests for certbot.plugins.common.""" +import functools +import os +import shutil +import tempfile import unittest +import josepy as jose import mock import OpenSSL from acme import challenges -from acme import jose from certbot import achallenges +from certbot import crypto_util +from certbot import errors 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")) +ACHALLS = [ + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.TLSSNI01(token=b'token1'), "pending"), + domain="encryption-example.demo", account_key=AUTH_KEY), + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.TLSSNI01(token=b'token2'), "pending"), + domain="certbot.demo", account_key=AUTH_KEY), +] class NamespaceFunctionsTest(unittest.TestCase): """Tests for certbot.plugins.common.*_namespace functions.""" @@ -73,6 +90,107 @@ "--mock-foo-bar", dest="different_to_foo_bar", x=1, y=None) +class InstallerTest(test_util.ConfigTestCase): + """Tests for certbot.plugins.common.Installer.""" + + def setUp(self): + super(InstallerTest, self).setUp() + os.mkdir(self.config.config_dir) + from certbot.plugins.common import Installer + + with mock.patch("certbot.plugins.common.reverter.Reverter"): + self.installer = Installer(config=self.config, + name="Installer") + self.reverter = self.installer.reverter + + def test_add_to_real_checkpoint(self): + files = set(("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 = set(("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 = self.reverter.add_to_temp_checkpoint + else: + reverter_func = self.reverter.add_to_checkpoint + + self._test_adapted_method( + installer_func, reverter_func, 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_view_config_changes(self): + self._test_wrapped_method("view_config_changes") + + 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) + reverter_func = getattr(self.reverter, name) + self._test_adapted_method( + installer_func, reverter_func, *args, **kwargs) + + def _test_adapted_method(self, installer_func, + reverter_func, *passed_args, **passed_kwargs): + """Test an adapted reverter method + + :param callable installer_func: installer method to test + :param mock.MagicMock reverter_func: mocked adapated + reverter method + :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 + + """ + installer_func(*passed_args, **passed_kwargs) + reverter_func.assert_called_once_with(*passed_args, **passed_kwargs) + reverter_func.side_effect = errors.ReverterError + self.assertRaises( + errors.PluginError, installer_func, *passed_args, **passed_kwargs) + + def test_install_ssl_dhparams(self): + self.installer.install_ssl_dhparams() + self.assertTrue(os.path.isfile(self.installer.ssl_dhparams)) + + def _current_ssl_dhparams_hash(self): + from certbot.constants import SSL_DHPARAMS_SRC + return crypto_util.sha256sum(SSL_DHPARAMS_SRC) + + def test_current_file_hash_in_all_hashes(self): + from certbot.constants import ALL_SSL_DHPARAMS_HASHES + self.assertTrue(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.client.plugins.common.Addr.""" @@ -154,29 +272,38 @@ self.assertEqual(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(ACHALLS[0], 0) + self.assertEqual(1, len(self.performer.achalls)) + self.assertEqual([0], self.performer.indices) + + def test_perform(self): + self.assertRaises(NotImplementedError, self.performer.perform) + + class TLSSNI01Test(unittest.TestCase): """Tests for certbot.plugins.common.TLSSNI01.""" - auth_key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) - achalls = [ - achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.chall_to_challb( - challenges.TLSSNI01(token=b'token1'), "pending"), - domain="encryption-example.demo", account_key=auth_key), - achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.chall_to_challb( - challenges.TLSSNI01(token=b'token2'), "pending"), - domain="certbot.demo", account_key=auth_key), - ] - def setUp(self): + self.tempdir = tempfile.mkdtemp() + configurator = mock.MagicMock() + configurator.config.config_dir = os.path.join(self.tempdir, "config") + configurator.config.work_dir = os.path.join(self.tempdir, "work") + from certbot.plugins.common import TLSSNI01 - self.sni = TLSSNI01(configurator=mock.MagicMock()) + self.sni = TLSSNI01(configurator=configurator) - def test_add_chall(self): - self.sni.add_chall(self.achalls[0], 0) - self.assertEqual(1, len(self.sni.achalls)) - self.assertEqual([0], self.sni.indices) + def tearDown(self): + shutil.rmtree(self.tempdir) def test_setup_challenge_cert(self): # This is a helper function that can be used for handling @@ -187,9 +314,10 @@ response = challenges.TLSSNI01Response() achall = mock.MagicMock() + achall.chall.encode.return_value = "token" key = test_util.load_pyopenssl_private_key("rsa512_key.pem") achall.response_and_validation.return_value = ( - response, (test_util.load_cert("cert.pem"), key)) + response, (test_util.load_cert("cert_512.pem"), key)) with mock.patch("certbot.plugins.common.open", mock_open, create=True): @@ -202,12 +330,93 @@ # pylint: disable=no-member mock_open.assert_called_once_with(self.sni.get_cert_path(achall), "wb") mock_open.return_value.write.assert_called_once_with( - test_util.load_vector("cert.pem")) + test_util.load_vector("cert_512.pem")) mock_safe_open.assert_called_once_with( self.sni.get_key_path(achall), "wb", chmod=0o400) mock_safe_open.return_value.write.assert_called_once_with( OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) + def test_get_z_domain(self): + achall = ACHALLS[0] + self.assertEqual(self.sni.get_z_domain(achall), + achall.response(achall.account_key).z_domain.decode("utf-8")) + + +class InstallVersionControlledFileTest(test_util.TempDirTestCase): + """Tests for certbot.plugins.common.install_version_controlled_file.""" + + def setUp(self): + super(InstallVersionControlledFileTest, self).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): + self.assertTrue(os.path.isfile(self.dest_path)) + self.assertEqual(crypto_util.sha256sum(self.dest_path), + self._current_file_hash()) + + def test_no_file(self): + self.assertFalse(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() + self.assertFalse(mock_logger.warning.called) + self.assertTrue(os.path.isfile(self.dest_path)) + self.assertEqual(crypto_util.sha256sum(self.source_path), + self._current_file_hash()) + self.assertNotEqual(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() + self.assertEqual(mock_logger.warning.call_args[0][0], + "%s has been manually modified; updated file " + "saved to %s. We recommend updating %s for security purposes.") + self.assertEqual(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() + self.assertFalse(mock_logger.warning.called) if __name__ == "__main__": unittest.main() # pragma: no cover diff -Nru python-certbot-0.10.2/certbot/plugins/disco.py python-certbot-0.28.0/certbot/plugins/disco.py --- python-certbot-0.10.2/certbot/plugins/disco.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/disco.py 2018-11-07 21:14:56.000000000 +0000 @@ -5,9 +5,12 @@ import pkg_resources import six +from collections import OrderedDict + import zope.interface import zope.interface.verify +from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module from certbot import constants from certbot import errors from certbot import interfaces @@ -22,12 +25,27 @@ PREFIX_FREE_DISTRIBUTIONS = [ "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", + "certbot-postfix", ] """Distributions for which prefix will be omitted.""" # this object is mutable, don't allow it to be hashed! - __hash__ = None + __hash__ = None # type: ignore def __init__(self, entry_point): self.name = self.entry_point_to_plugin_name(entry_point) @@ -79,7 +97,7 @@ return self._initialized is not None def init(self, config=None): - """Memoized plugin inititialization.""" + """Memoized plugin initialization.""" if not self.initialized: self.entry_point.require() # fetch extras! self._initialized = self.plugin_cls(config, self.name) @@ -168,12 +186,17 @@ """Plugins registry.""" def __init__(self, plugins): - self._plugins = plugins + # 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 = OrderedDict(sorted(six.iteritems(plugins))) @classmethod def find_all(cls): """Find plugins using setuptools entry points.""" - plugins = {} + plugins = {} # type: Dict[str, PluginEntryPoint] + # pylint: disable=not-callable entry_points = itertools.chain( pkg_resources.iter_entry_points( constants.SETUPTOOLS_PLUGINS_ENTRY_POINT), @@ -230,7 +253,7 @@ def available(self): """Filter plugins based on availability.""" return self.filter(lambda p_ep: p_ep.available) - # succefully prepared + misconfigured + # successfully prepared + misconfigured def find_init(self, plugin): """Find an initialized plugin. diff -Nru python-certbot-0.10.2/certbot/plugins/disco_test.py python-certbot-0.28.0/certbot/plugins/disco_test.py --- python-certbot-0.10.2/certbot/plugins/disco_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/disco_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,4 +1,6 @@ """Tests for certbot.plugins.disco.""" +import functools +import string import unittest import mock @@ -6,6 +8,7 @@ import six import zope.interface +from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module from certbot import errors from certbot import interfaces @@ -182,12 +185,17 @@ class PluginsRegistryTest(unittest.TestCase): """Tests for certbot.plugins.disco.PluginsRegistry.""" - def setUp(self): + @classmethod + def _create_new_registry(cls, plugins): from certbot.plugins.disco import PluginsRegistry - self.plugin_ep = mock.MagicMock(name="mock") + 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 = {"mock": self.plugin_ep} - self.reg = PluginsRegistry(self.plugins) + self.plugins = {self.plugin_ep.name: self.plugin_ep} + self.reg = self._create_new_registry(self.plugins) def test_find_all(self): from certbot.plugins.disco import PluginsRegistry @@ -207,9 +215,8 @@ self.assertEqual(["mock"], list(self.reg)) def test_len(self): + self.assertEqual(0, len(self._create_new_registry({}))) self.assertEqual(1, len(self.reg)) - self.plugins.clear() - self.assertEqual(0, len(self.reg)) def test_init(self): self.plugin_ep.init.return_value = "baz" @@ -217,14 +224,11 @@ self.plugin_ep.init.assert_called_once_with("bar") def test_filter(self): - self.plugins.update({ - "foo": "bar", - "bar": "foo", - "baz": "boo", - }) self.assertEqual( - {"foo": "bar", "baz": "boo"}, - self.reg.filter(lambda p_ep: str(p_ep).startswith("b"))) + self.plugins, + self.reg.filter(lambda p_ep: p_ep.name.startswith("m"))) + self.assertEqual( + {}, self.reg.filter(lambda p_ep: p_ep.name.startswith("b"))) def test_ifaces(self): self.plugin_ep.ifaces.return_value = True @@ -246,6 +250,17 @@ self.assertEqual(["baz"], self.reg.prepare()) self.plugin_ep.prepare.assert_called_once_with() + def test_prepare_order(self): + order = [] # type: List[str] + plugins = dict( + (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 + self.assertEqual(order, sorted(string.ascii_letters)) + def test_available(self): self.plugin_ep.available = True # pylint: disable=protected-access @@ -255,7 +270,7 @@ def test_find_init(self): self.assertTrue(self.reg.find_init(mock.Mock()) is None) - self.plugin_ep.initalized = True + self.plugin_ep.initialized = True self.assertTrue( self.reg.find_init(self.plugin_ep.init()) is self.plugin_ep) @@ -265,11 +280,12 @@ repr(self.reg)) def test_str(self): + self.assertEqual("No plugins", str(self._create_new_registry({}))) self.plugin_ep.__str__ = lambda _: "Mock" - self.plugins["foo"] = "Mock" - self.assertEqual("Mock\n\nMock", str(self.reg)) - self.plugins.clear() - self.assertEqual("No plugins", str(self.reg)) + self.assertEqual("Mock", str(self.reg)) + plugins = {self.plugin_ep.name: self.plugin_ep, "foo": "Bar"} + reg = self._create_new_registry(plugins) + self.assertEqual("Bar\n\nMock", str(reg)) if __name__ == "__main__": diff -Nru python-certbot-0.10.2/certbot/plugins/dns_common.py python-certbot-0.28.0/certbot/plugins/dns_common.py --- python-certbot-0.10.2/certbot/plugins/dns_common.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/dns_common.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,335 @@ +"""Common code for DNS Authenticator Plugins.""" + +import abc +import logging +import os +import stat +from time import sleep + +import configobj +import zope.interface +from acme import challenges + +from certbot import errors +from certbot import interfaces +from certbot.display import ops +from certbot.display import util as display_util +from certbot.plugins import common + +logger = logging.getLogger(__name__) + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class DNSAuthenticator(common.Plugin): + """Base class for DNS Authenticators""" + + def __init__(self, config, name): + super(DNSAuthenticator, self).__init__(config, name) + + self._attempt_cleanup = False + + @classmethod + def add_parser_arguments(cls, add, default_propagation_seconds=10): # pylint: disable=arguments-differ + 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 get_chall_pref(self, unused_domain): # pylint: disable=missing-docstring,no-self-use + return [challenges.DNS01] + + def prepare(self): # pylint: disable=missing-docstring + pass + + def perform(self, achalls): # pylint: disable=missing-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. + logger.info("Waiting %d seconds for DNS changes to propagate", + self.conf('propagation-seconds')) + sleep(self.conf('propagation-seconds')) + + return responses + + def cleanup(self, achalls): # pylint: disable=missing-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): # pragma: no cover + """ + Establish credentials, prompting if necessary. + """ + raise NotImplementedError() + + @abc.abstractmethod + def _perform(self, domain, validation_domain_name, validation): # 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, validation_domain_name, validation): # 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, label): + """ + 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, label, validator=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, label, required_variables=None, validator=None): + """ + 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): + configuration = CredentialsConfiguration(filename, self.dest) + + if required_variables: + configuration.require(required_variables) + + if validator: + validator(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): + """ + 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): + 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 + else: + raise errors.PluginError('{0} required to proceed.'.format(label)) + + @staticmethod + def _prompt_for_file(label, validator=None): + """ + 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): + 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 + else: + raise errors.PluginError('{0} required to proceed.'.format(label)) + + +class CredentialsConfiguration(object): + """Represents a user-supplied filed which stores API credentials.""" + + def __init__(self, filename, mapper=lambda x: x): + """ + :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", e, exc_info=True) + raise errors.PluginError("Error parsing credentials configuration: {0}".format(e)) + + self.mapper = mapper + + def require(self, required_variables): + """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): + """Find a configuration value for variable `var`, as transformed by `mapper`. + + :param str var: The variable to get. + :returns: The value of the variable. + :rtype: str + """ + + return self._get(var) + + def _has(self, var): + return self.mapper(var) in self.confobj + + def _get(self, var): + return self.confobj.get(self.mapper(var)) + + +def validate_file(filename): + """Ensure that the specified file exists.""" + + if not os.path.exists(filename): + raise errors.PluginError('File not found: {0}'.format(filename)) + + if not os.path.isfile(filename): + raise errors.PluginError('Path is not a file: {0}'.format(filename)) + + +def validate_file_permissions(filename): + """Ensure that the specified file exists and warn about unsafe permissions.""" + + validate_file(filename) + + permissions = stat.S_IMODE(os.stat(filename).st_mode) + if permissions & stat.S_IRWXO: + logger.warning('Unsafe permissions on credentials configuration file: %s', filename) + + +def base_domain_name_guesses(domain): + """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))] diff -Nru python-certbot-0.10.2/certbot/plugins/dns_common_lexicon.py python-certbot-0.28.0/certbot/plugins/dns_common_lexicon.py --- python-certbot-0.10.2/certbot/plugins/dns_common_lexicon.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/dns_common_lexicon.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,102 @@ +"""Common code for DNS Authenticator Plugins built on Lexicon.""" + +import logging + +from requests.exceptions import HTTPError, RequestException + +from certbot import errors +from certbot.plugins import dns_common + +logger = logging.getLogger(__name__) + + +class LexiconClient(object): + """ + Encapsulates all communication with a DNS provider via Lexicon. + """ + + def __init__(self): + self.provider = None + + def add_txt_record(self, domain, record_name, record_content): + """ + 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(type='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, record_name, record_content): + """ + 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(type='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): + """ + 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: + result = self._handle_http_error(e, domain_name) + + if result: + raise result + except Exception as e: # pylint: disable=broad-except + result = self._handle_general_error(e, domain_name) + + if result: + raise result + + 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, domain_name): + return errors.PluginError('Error determining zone identifier for {0}: {1}.' + .format(domain_name, e)) + + def _handle_general_error(self, e, domain_name): + if not str(e).startswith('No domain found'): + return errors.PluginError('Unexpected error determining zone identifier for {0}: {1}' + .format(domain_name, e)) diff -Nru python-certbot-0.10.2/certbot/plugins/dns_common_lexicon_test.py python-certbot-0.28.0/certbot/plugins/dns_common_lexicon_test.py --- python-certbot-0.10.2/certbot/plugins/dns_common_lexicon_test.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/dns_common_lexicon_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,27 @@ +"""Tests for certbot.plugins.dns_common_lexicon.""" + +import unittest + +import mock + +from certbot.plugins import dns_common_lexicon +from certbot.plugins import dns_test_common_lexicon + + +class LexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): + + class _FakeLexiconClient(dns_common_lexicon.LexiconClient): + pass + + def setUp(self): + super(LexiconClientTest, self).setUp() + + self.client = LexiconClientTest._FakeLexiconClient() + self.provider_mock = mock.MagicMock() + + self.client.provider = self.provider_mock + + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff -Nru python-certbot-0.10.2/certbot/plugins/dns_common_test.py python-certbot-0.28.0/certbot/plugins/dns_common_test.py --- python-certbot-0.10.2/certbot/plugins/dns_common_test.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/dns_common_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,233 @@ +"""Tests for certbot.plugins.dns_common.""" + +import collections +import logging +import os +import unittest + +import mock + +from certbot import errors +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 + + +class DNSAuthenticatorTest(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 __init__(self, *args, **kwargs): + # pylint: disable=protected-access + super(DNSAuthenticatorTest._FakeDNSAuthenticator, self).__init__(*args, **kwargs) + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return 'A fake authenticator for testing.' + + class _FakeConfig(object): + fake_propagation_seconds = 0 + fake_config_key = 1 + fake_other_key = None + fake_file_path = None + + def setUp(self): + super(DNSAuthenticatorTest, self).setUp() + + self.config = DNSAuthenticatorTest._FakeConfig() + + self.auth = DNSAuthenticatorTest._FakeDNSAuthenticator(self.config, "fake") + + def test_perform(self): + 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) + + @util.patch_get_utility() + 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", "") + self.assertEqual(self.auth.config.fake_other_key, "value") + + @util.patch_get_utility() + def test_prompt_canceled(self, mock_get_utility): + mock_display = mock_get_utility() + mock_display.input.side_effect = ((display_util.CANCEL, "c",),) + + self.assertRaises(errors.PluginError, self.auth._configure, "other_key", "") + + @util.patch_get_utility() + 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", "") + self.assertEqual(self.auth.config.fake_file_path, path) + + @util.patch_get_utility() + def test_prompt_file_canceled(self, mock_get_utility): + mock_display = mock_get_utility() + mock_display.directory_select.side_effect = ((display_util.CANCEL, "c",),) + + self.assertRaises(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": ""}) + + self.assertEqual(credentials.conf("test"), "value") + + @util.patch_get_utility() + 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": ""}) + self.assertEqual(credentials.conf("test"), "value") + + +class CredentialsConfigurationTest(util.TempDirTestCase): + class _MockLoggingHandler(logging.Handler): + messages = None + + def __init__(self, *args, **kwargs): + self.reset() + logging.Handler.__init__(self, *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) + self.assertEqual("value", credentials_configuration.conf("test")) + self.assertEqual("1", credentials_configuration.conf("other")) + + def test_nonexistent_file(self): + path = os.path.join(self.tempdir, 'not-a-file.ini') + + self.assertRaises(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') + open(path, "wb").close() + + dns_common.CredentialsConfiguration(path) + + self.assertEqual(1, len([_ for _ in log.messages['warning'] if _.startswith("Unsafe")])) + + +class CredentialsConfigurationRequireTest(util.TempDirTestCase): + + def setUp(self): + super(CredentialsConfigurationRequireTest, self).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) + self.assertRaises(errors.PluginError, credentials_configuration.require, {"test": ""}) + + def test_blank(self): + self._write({"test": ""}) + + credentials_configuration = dns_common.CredentialsConfiguration(self.path) + self.assertRaises(errors.PluginError, credentials_configuration.require, {"test": ""}) + + def test_typo(self): + self._write({"tets": "typo!"}) + + credentials_configuration = dns_common.CredentialsConfiguration(self.path) + self.assertRaises(errors.PluginError, credentials_configuration.require, {"test": ""}) + + +class DomainNameGuessTest(unittest.TestCase): + + def test_simple_case(self): + self.assertTrue( + 'example.com' in + dns_common.base_domain_name_guesses("example.com") + ) + + def test_sub_domain(self): + self.assertTrue( + 'example.com' in + dns_common.base_domain_name_guesses("foo.bar.baz.example.com") + ) + + def test_second_level_domain(self): + self.assertTrue( + 'example.co.uk' in + dns_common.base_domain_name_guesses("foo.bar.baz.example.co.uk") + ) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff -Nru python-certbot-0.10.2/certbot/plugins/dns_test_common.py python-certbot-0.28.0/certbot/plugins/dns_test_common.py --- python-certbot-0.10.2/certbot/plugins/dns_test_common.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/dns_test_common.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,63 @@ +"""Base test class for DNS authenticators.""" + +import os + +import configobj +import josepy as jose +import mock +import six +from acme import challenges + +from certbot import achallenges +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 BaseAuthenticatorTest(object): + """ + 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): + # pylint: disable=no-member + self.assertTrue(isinstance(self.auth.more_info(), six.string_types)) + + def test_get_chall_pref(self): + # pylint: disable=no-member + self.assertEqual(self.auth.get_chall_pref(None), [challenges.DNS01]) + + def test_parser_arguments(self): + m = mock.MagicMock() + + # pylint: disable=no-member + self.auth.add_parser_arguments(m) + + m.assert_any_call('propagation-seconds', type=int, default=mock.ANY, help=mock.ANY) + + +def write(values, path): + """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) + + os.chmod(path, 0o600) diff -Nru python-certbot-0.10.2/certbot/plugins/dns_test_common_lexicon.py python-certbot-0.28.0/certbot/plugins/dns_test_common_lexicon.py --- python-certbot-0.10.2/certbot/plugins/dns_test_common_lexicon.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/dns_test_common_lexicon.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,128 @@ +"""Base test class for DNS authenticators built on Lexicon.""" + +import josepy as jose +import mock +from requests.exceptions import HTTPError, RequestException + +from certbot import errors +from certbot.plugins import dns_test_common +from certbot.tests import util as test_util + +DOMAIN = 'example.com' +KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) + +# 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): + + def test_perform(self): + 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): + 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(object): + DOMAIN_NOT_FOUND = Exception('No domain found') + GENERIC_ERROR = RequestException + LOGIN_ERROR = HTTPError('400 Client Error: ...') + UNKNOWN_LOGIN_ERROR = HTTPError('500 Surprise! Error: ...') + + record_prefix = "_acme-challenge" + record_name = record_prefix + "." + DOMAIN + record_content = "bar" + + def test_add_txt_record(self): + self.client.add_txt_record(DOMAIN, self.record_name, self.record_content) + + self.provider_mock.create_record.assert_called_with(type='TXT', + name=self.record_name, + content=self.record_content) + + def test_add_txt_record_try_twice_to_find_domain(self): + 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(type='TXT', + name=self.record_name, + content=self.record_content) + + def test_add_txt_record_fail_to_find_domain(self): + 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): + 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): + 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): + 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): + 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): + self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) + + self.provider_mock.delete_record.assert_called_with(type='TXT', + name=self.record_name, + content=self.record_content) + + def test_del_txt_record_fail_to_find_domain(self): + 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): + 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): + 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): + 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): + self.provider_mock.delete_record.side_effect = self.GENERIC_ERROR + + self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) diff -Nru python-certbot-0.10.2/certbot/plugins/enhancements.py python-certbot-0.28.0/certbot/plugins/enhancements.py --- python-certbot-0.10.2/certbot/plugins/enhancements.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/enhancements.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,164 @@ +"""New interface style Certbot enhancements""" +import abc +import six + +from certbot import constants + +from acme.magic_typing import Dict, List, Any # pylint: disable=unused-import, no-name-in-module + +def enabled_enhancements(config): + """ + Generator to yield the enabled new style enhancements. + + :param config: Configuration. + :type config: :class:`certbot.interfaces.IConfig` + """ + for enh in _INDEX: + if getattr(config, enh["cli_dest"]): + yield enh + +def are_requested(config): + """ + Checks if one or more of the requested enhancements are those of the new + enhancement interfaces. + + :param config: Configuration. + :type config: :class:`certbot.interfaces.IConfig` + """ + return any(enabled_enhancements(config)) + +def are_supported(config, installer): + """ + Checks that all of the requested enhancements are supported by the + installer. + + :param config: Configuration. + :type config: :class:`certbot.interfaces.IConfig` + + :param installer: Installer object + :type installer: interfaces.IInstaller + + :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, domains, installer, config): + """ + Run enable method for each requested enhancement that is supported. + + :param lineage: Certificate lineage object + :type lineage: certbot.storage.RenewableCert + + :param domains: List of domains in certificate to enhance + :type domains: str + + :param installer: Installer object + :type installer: interfaces.IInstaller + + :param config: Configuration. + :type config: :class:`certbot.interfaces.IConfig` + """ + for enh in enabled_enhancements(config): + getattr(installer, enh["enable_function"])(lineage, domains) + +def populate_cli(add): + """ + Populates the command line flags for certbot.cli.HelpfulParser + + :param add: Add function of certbot.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"]) + + +@six.add_metaclass(abc.ABCMeta) +class AutoHSTSEnhancement(object): + """ + 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, *args, **kwargs): + """ + 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.storage.RenewableCert + + .. note:: prepare() method inherited from `interfaces.IPlugin` might need + to be called manually within implementation of this interface method + to finalize the plugin initialization. + """ + + @abc.abstractmethod + def deploy_autohsts(self, lineage, *args, **kwargs): + """ + 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.storage.RenewableCert + """ + + @abc.abstractmethod + def enable_autohsts(self, lineage, domains, *args, **kwargs): + """ + 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.storage.RenewableCert + + :param domains: List of domains in certificate to enhance + :type domains: 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 = [ + { + "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" + } +] # type: List[Dict[str, Any]] diff -Nru python-certbot-0.10.2/certbot/plugins/enhancements_test.py python-certbot-0.28.0/certbot/plugins/enhancements_test.py --- python-certbot-0.10.2/certbot/plugins/enhancements_test.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/enhancements_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,65 @@ +"""Tests for new style enhancements""" +import unittest +import mock + +from certbot.plugins import enhancements +from certbot.plugins import null + +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(EnhancementTest, self).setUp() + self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement) + + + @test_util.patch_get_utility() + 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)) + self.assertEqual(len(enabled), 2) + self.assertTrue([i for i in enabled if i["name"] == "autohsts"]) + self.assertTrue([i for i in enabled if i["name"] == "somethingelse"]) + + def test_are_requested(self): + self.assertEquals( + len([i for i in enhancements.enabled_enhancements(self.config)]), 0) + self.assertFalse(enhancements.are_requested(self.config)) + self.config.auto_hsts = True + self.assertEquals( + len([i for i in enhancements.enabled_enhancements(self.config)]), 1) + self.assertTrue(enhancements.are_requested(self.config)) + + def test_are_supported(self): + self.config.auto_hsts = True + unsupported = null.Installer(self.config, "null") + self.assertTrue(enhancements.are_supported(self.config, self.mockinstaller)) + self.assertFalse(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) + self.assertTrue(self.mockinstaller.enable_autohsts.called) + self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0], + (lineage, domains)) + + +if __name__ == '__main__': + unittest.main() # pragma: no cover diff -Nru python-certbot-0.10.2/certbot/plugins/manual.py python-certbot-0.28.0/certbot/plugins/manual.py --- python-certbot-0.10.2/certbot/plugins/manual.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/manual.py 2018-11-07 21:14:56.000000000 +0000 @@ -5,13 +5,44 @@ import zope.interface from acme import challenges +from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module +from certbot import achallenges # pylint: disable=unused-import from certbot import interfaces from certbot import errors from certbot import hooks +from certbot import reverter from certbot.plugins import common +class ManualTlsSni01(common.TLSSNI01): + """TLS-SNI-01 authenticator for the Manual plugin + + :ivar configurator: Authenticator object + :type configurator: :class:`~certbot.plugins.manual.Authenticator` + + :ivar list achalls: Annotated + class:`~certbot.achallenges.KeyAuthorizationAnnotatedChallenge` + challenges + + :param list indices: Meant to hold indices of challenges in a + larger array. NginxTlsSni01 is capable of solving many challenges + at once which causes an indexing issue within NginxConfigurator + who must return all responses in order. Imagine NginxConfigurator + maintaining state about where all of the http-01 Challenges, + TLS-SNI-01 Challenges belong in the response array. This is an + optional utility. + + :param str challenge_conf: location of the challenge config file + """ + + def perform(self): + """Create the SSL certificates and private keys""" + + for achall in self.achalls: + self._setup_challenge_cert(achall) + + @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): @@ -28,42 +59,62 @@ 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 are ' - '$CERTBOT_DOMAIN which contains the domain being authenticated, ' - '$CERTBOT_VALIDATION which is the validation string, and ' - '$CERTBOT_TOKEN which 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.') + '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. When ' + 'performing a TLS-SNI-01 challenge, $CERTBOT_SNI_DOMAIN will contain ' + 'the SNI name for which the ACME server expects to be presented with ' + 'the self-signed certificate located at $CERTBOT_CERT_PATH. The ' + 'secret key needed to complete the TLS handshake is located at ' + '$CERTBOT_KEY_PATH. 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.') _DNS_INSTRUCTIONS = """\ Please deploy a DNS TXT record under the name {domain} with the following value: {validation} -Once this is deployed,""" +Before continuing, verify the record is deployed.""" _HTTP_INSTRUCTIONS = """\ -Make sure your web server displays the following content at -{uri} before continuing: +Create a file containing just this data: {validation} -If you don't have HTTP server configured, you can run the following -command on the target server (as root): +And make it available on your web server at this URL: -mkdir -p /tmp/certbot/public_html/{achall.URI_ROOT_PATH} -cd /tmp/certbot/public_html -printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token} -# run only once per server: -$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ -"import BaseHTTPServer, SimpleHTTPServer; \\ -s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ -s.serve_forever()" """ +{uri} +""" + _TLSSNI_INSTRUCTIONS = """\ +Configure the service listening on port {port} to present the certificate +{cert} +using the secret key +{key} +when it receives a TLS ClientHello with the SNI extension set to +{sni_domain} +""" + _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, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - self.env = dict() + self.reverter = reverter.Reverter(self.config) + self.reverter.recovery_routine() + self.env = dict() \ + # type: Dict[achallenges.KeyAuthorizationAnnotatedChallenge, Dict[str, str]] + self.tls_sni_01 = None + self.subsequent_dns_challenge = False + self.subsequent_any_challenge = False @classmethod def add_parser_arguments(cls, add): @@ -98,11 +149,10 @@ def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.HTTP01, challenges.DNS01] + return [challenges.HTTP01, challenges.DNS01, challenges.TLSSNI01] def perform(self, achalls): # pylint: disable=missing-docstring self._verify_ip_logging_ok() - if self.conf('auth-hook'): perform_achall = self._perform_achall_with_script else: @@ -110,6 +160,12 @@ responses = [] for achall in achalls: + if isinstance(achall.chall, challenges.TLSSNI01): + # Make a new ManualTlsSni01 instance for each challenge + # because the manual plugin deals with one challenge at a time. + self.tls_sni_01 = ManualTlsSni01(self) + self.tls_sni_01.add_chall(achall) + self.tls_sni_01.perform() perform_achall(achall) responses.append(achall.response(achall.account_key)) return responses @@ -135,10 +191,20 @@ env['CERTBOT_TOKEN'] = achall.chall.encode('token') else: os.environ.pop('CERTBOT_TOKEN', None) + if isinstance(achall.chall, challenges.TLSSNI01): + env['CERTBOT_CERT_PATH'] = self.tls_sni_01.get_cert_path(achall) + env['CERTBOT_KEY_PATH'] = self.tls_sni_01.get_key_path(achall) + env['CERTBOT_SNI_DOMAIN'] = self.tls_sni_01.get_z_domain(achall) + os.environ.pop('CERTBOT_VALIDATION', None) + env.pop('CERTBOT_VALIDATION') + else: + os.environ.pop('CERTBOT_CERT_PATH', None) + os.environ.pop('CERTBOT_KEY_PATH', None) + os.environ.pop('CERTBOT_SNI_DOMAIN', None) os.environ.update(env) _, out = hooks.execute(self.conf('auth-hook')) env['CERTBOT_AUTH_OUTPUT'] = out.strip() - self.env[achall.domain] = env + self.env[achall] = env def _perform_achall_manually(self, achall): validation = achall.validation(achall.account_key) @@ -147,19 +213,35 @@ 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) + elif isinstance(achall.chall, challenges.DNS01): msg = self._DNS_INSTRUCTIONS.format( domain=achall.validation_domain_name(achall.domain), validation=validation) + else: + assert isinstance(achall.chall, challenges.TLSSNI01) + msg = self._TLSSNI_INSTRUCTIONS.format( + cert=self.tls_sni_01.get_cert_path(achall), + key=self.tls_sni_01.get_key_path(achall), + port=self.config.tls_sni_01_port, + sni_domain=self.tls_sni_01.get_z_domain(achall)) + if isinstance(achall.chall, challenges.DNS01): + if self.subsequent_dns_challenge: + # 2nd or later dns-01 challenge + msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS + self.subsequent_dns_challenge = True + elif self.subsequent_any_challenge: + # 2nd or later challenge of another type + msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS display = zope.component.getUtility(interfaces.IDisplay) display.notification(msg, wrap=False, force_interactive=True) + self.subsequent_any_challenge = True def cleanup(self, achalls): # pylint: disable=missing-docstring if self.conf('cleanup-hook'): for achall in achalls: - env = self.env.pop(achall.domain) + env = self.env.pop(achall) if 'CERTBOT_TOKEN' not in env: os.environ.pop('CERTBOT_TOKEN', None) os.environ.update(env) hooks.execute(self.conf('cleanup-hook')) + self.reverter.recovery_routine() diff -Nru python-certbot-0.10.2/certbot/plugins/manual_test.py python-certbot-0.28.0/certbot/plugins/manual_test.py --- python-certbot-0.10.2/certbot/plugins/manual_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/manual_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -4,6 +4,7 @@ import six import mock +import sys from acme import challenges @@ -13,17 +14,32 @@ from certbot.tests import util as test_util -class AuthenticatorTest(unittest.TestCase): +class AuthenticatorTest(test_util.TempDirTestCase): """Tests for certbot.plugins.manual.Authenticator.""" def setUp(self): + super(AuthenticatorTest, self).setUp() self.http_achall = acme_util.HTTP01_A self.dns_achall = acme_util.DNS01_A - self.achalls = [self.http_achall, self.dns_achall] + self.dns_achall_2 = acme_util.DNS01_A_2 + self.tls_sni_achall = acme_util.TLSSNI01_A + self.achalls = [self.http_achall, self.dns_achall, self.tls_sni_achall, self.dns_achall_2] + for d in ["config_dir", "work_dir", "in_progress"]: + os.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, manual_public_ip_logging_ok=False, noninteractive_mode=False, - validate_hooks=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"), + tls_sni_01_port=5001) from certbot.plugins.manual import Authenticator self.auth = Authenticator(self.config, name='manual') @@ -42,7 +58,9 @@ def test_get_chall_pref(self): self.assertEqual(self.auth.get_chall_pref('example.org'), - [challenges.HTTP01, challenges.DNS01]) + [challenges.HTTP01, + challenges.DNS01, + challenges.TLSSNI01]) @test_util.patch_get_utility() def test_ip_logging_not_ok(self, mock_get_utility): @@ -58,24 +76,43 @@ def test_script_perform(self): self.config.manual_public_ip_logging_ok = True self.config.manual_auth_hook = ( - 'echo $CERTBOT_DOMAIN; echo ${CERTBOT_TOKEN:-notoken}; ' - 'echo $CERTBOT_VALIDATION;') - dns_expected = '{0}\n{1}\n{2}'.format( + '{0} -c "from __future__ import print_function;' + 'import os; print(os.environ.get(\'CERTBOT_DOMAIN\'));' + 'print(os.environ.get(\'CERTBOT_TOKEN\', \'notoken\'));' + 'print(os.environ.get(\'CERTBOT_CERT_PATH\', \'nocert\'));' + 'print(os.environ.get(\'CERTBOT_KEY_PATH\', \'nokey\'));' + 'print(os.environ.get(\'CERTBOT_SNI_DOMAIN\', \'nosnidomain\'));' + 'print(os.environ.get(\'CERTBOT_VALIDATION\', \'novalidation\'));"' + .format(sys.executable)) + dns_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format( self.dns_achall.domain, 'notoken', + 'nocert', 'nokey', 'nosnidomain', self.dns_achall.validation(self.dns_achall.account_key)) - http_expected = '{0}\n{1}\n{2}'.format( + http_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format( self.http_achall.domain, self.http_achall.chall.encode('token'), + 'nocert', 'nokey', 'nosnidomain', self.http_achall.validation(self.http_achall.account_key)) self.assertEqual( self.auth.perform(self.achalls), [achall.response(achall.account_key) for achall in self.achalls]) self.assertEqual( - self.auth.env[self.dns_achall.domain]['CERTBOT_AUTH_OUTPUT'], + self.auth.env[self.dns_achall]['CERTBOT_AUTH_OUTPUT'], dns_expected) self.assertEqual( - self.auth.env[self.http_achall.domain]['CERTBOT_AUTH_OUTPUT'], + self.auth.env[self.http_achall]['CERTBOT_AUTH_OUTPUT'], http_expected) + # tls_sni_01 challenge must be perform()ed above before we can + # get the cert_path and key_path. + tls_sni_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format( + self.tls_sni_achall.domain, 'notoken', + self.auth.tls_sni_01.get_cert_path(self.tls_sni_achall), + self.auth.tls_sni_01.get_key_path(self.tls_sni_achall), + self.auth.tls_sni_01.get_z_domain(self.tls_sni_achall), + 'novalidation') + self.assertEqual( + self.auth.env[self.tls_sni_achall]['CERTBOT_AUTH_OUTPUT'], + tls_sni_expected) @test_util.patch_get_utility() def test_manual_perform(self, mock_get_utility): @@ -85,9 +122,16 @@ [achall.response(achall.account_key) for achall in self.achalls]) for i, (args, kwargs) in enumerate(mock_get_utility().notification.call_args_list): achall = self.achalls[i] - self.assertTrue(achall.validation(achall.account_key) in args[0]) + if isinstance(achall.chall, challenges.TLSSNI01): + self.assertTrue( + self.auth.tls_sni_01.get_cert_path( + self.tls_sni_achall) in args[0]) + else: + self.assertTrue( + achall.validation(achall.account_key) in args[0]) self.assertFalse(kwargs['wrap']) + @test_util.broken_on_windows def test_cleanup(self): self.config.manual_public_ip_logging_ok = True self.config.manual_auth_hook = 'echo foo;' @@ -98,16 +142,29 @@ self.auth.cleanup([achall]) self.assertEqual(os.environ['CERTBOT_AUTH_OUTPUT'], 'foo') self.assertEqual(os.environ['CERTBOT_DOMAIN'], achall.domain) - self.assertEqual( - os.environ['CERTBOT_VALIDATION'], - achall.validation(achall.account_key)) - + if (isinstance(achall.chall, challenges.HTTP01) or + isinstance(achall.chall, challenges.DNS01)): + self.assertEqual( + os.environ['CERTBOT_VALIDATION'], + achall.validation(achall.account_key)) if isinstance(achall.chall, challenges.HTTP01): self.assertEqual( os.environ['CERTBOT_TOKEN'], achall.chall.encode('token')) else: self.assertFalse('CERTBOT_TOKEN' in os.environ) + if isinstance(achall.chall, challenges.TLSSNI01): + self.assertEqual( + os.environ['CERTBOT_CERT_PATH'], + self.auth.tls_sni_01.get_cert_path(achall)) + self.assertEqual( + os.environ['CERTBOT_KEY_PATH'], + self.auth.tls_sni_01.get_key_path(achall)) + self.assertFalse( + os.path.exists(os.environ['CERTBOT_CERT_PATH'])) + self.assertFalse( + os.path.exists(os.environ['CERTBOT_KEY_PATH'])) + if __name__ == '__main__': diff -Nru python-certbot-0.10.2/certbot/plugins/null_test.py python-certbot-0.28.0/certbot/plugins/null_test.py --- python-certbot-0.10.2/certbot/plugins/null_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/null_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,5 +1,6 @@ """Tests for certbot.plugins.null.""" import unittest +import six import mock @@ -12,7 +13,7 @@ self.installer = Installer(config=mock.MagicMock(), name="null") def test_it(self): - self.assertTrue(isinstance(self.installer.more_info(), str)) + self.assertTrue(isinstance(self.installer.more_info(), six.string_types)) self.assertEqual([], self.installer.get_all_names()) self.assertEqual([], self.installer.supported_enhancements()) diff -Nru python-certbot-0.10.2/certbot/plugins/selection.py python-certbot-0.28.0/certbot/plugins/selection.py --- python-certbot-0.10.2/certbot/plugins/selection.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/selection.py 2018-11-07 21:14:56.000000000 +0000 @@ -39,6 +39,35 @@ return pick_plugin( config, default, plugins, question, (interfaces.IAuthenticator,)) +def get_unprepared_installer(config, plugins): + """ + Get an unprepared interfaces.IInstaller object. + + :param certbot.interfaces.IConfig config: Configuration + :param certbot.plugins.disco.PluginsRegistry plugins: + All plugins registered as entry points. + + :returns: Unprepared installer plugin or None + :rtype: IPlugin or None + """ + + _, req_inst = cli_plugin_requests(config) + if not req_inst: + return None + installers = plugins.filter(lambda p_ep: p_ep.name == req_inst) + installers.init(config) + installers = installers.verify((interfaces.IInstaller,)) + 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) + else: + raise errors.PluginSelectionError( + "Could not select or initialize the requested installer %s." % req_inst) def pick_plugin(config, default, plugins, question, ifaces): """Pick plugin. @@ -108,11 +137,19 @@ opts = [plugin_ep.description_with_name + (" [Misconfigured]" if plugin_ep.misconfigured else "") for plugin_ep in prepared] + names = set(plugin_ep.name for plugin_ep in prepared) while True: disp = z_util(interfaces.IDisplay) - code, index = disp.menu( - question, opts, help_label="More Info", force_interactive=True) + if "CERTBOT_AUTO" in os.environ and names == set(("apache", "nginx")): + # The possibility of being offered exactly apache and nginx here + # is new interactivity brought by https://github.com/certbot/certbot/issues/4079, + # so set apache as a default for those kinds of non-interactive use + # (the user will get a warning to set --non-interactive or --force-interactive) + apache_idx = [n for n, p in enumerate(prepared) if p.name == "apache"][0] + code, index = disp.menu(question, opts, default=apache_idx) + else: + code, index = disp.menu(question, opts, force_interactive=True) if code == display_util.OK: plugin_ep = prepared[index] @@ -123,26 +160,24 @@ "was:\n\n{0}".format(plugin_ep.prepare()), pause=False) else: return plugin_ep - elif code == display_util.HELP: - if prepared[index].misconfigured: - msg = "Reported Error: %s" % prepared[index].prepare() - else: - msg = prepared[index].init().more_info() - z_util(interfaces.IDisplay).notification(msg, - force_interactive=True) else: return None -noninstaller_plugins = ["webroot", "manual", "standalone"] +noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", + "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, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." - cn = config.namespace - cn.authenticator = plugins.find_init(auth).name if auth else "None" - cn.installer = plugins.find_init(inst).name if inst else "None" + config.authenticator = plugins.find_init(auth).name if auth else None + config.installer = plugins.find_init(inst).name if inst else None + logger.info("Plugins selected: Authenticator %s, Installer %s", + config.authenticator, config.installer) def choose_configurator_plugins(config, plugins, verb): + # pylint: disable=too-many-branches """ Figure out which configurator we're going to use, modifies config.authenticator and config.installer strings to reflect that choice if @@ -155,6 +190,11 @@ """ req_auth, req_inst = cli_plugin_requests(config) + installer_question = None + + 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": @@ -172,11 +212,11 @@ need_inst = need_auth = False if verb == "certonly": need_auth = True - if verb == "install": + if verb == "install" or verb == "enhance": need_inst = True if config.authenticator: - logger.warning("Specifying an authenticator doesn't make sense in install mode") - + 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 = installer = None if verb == "run" and req_auth == req_inst: @@ -185,7 +225,7 @@ authenticator = installer = pick_configurator(config, req_inst, plugins) else: if need_inst or req_inst: - installer = pick_installer(config, req_inst, plugins) + installer = pick_installer(config, req_inst, plugins, installer_question) if need_auth: authenticator = pick_authenticator(config, req_auth, plugins) logger.debug("Selected authenticator %s and installer %s", authenticator, installer) @@ -216,7 +256,7 @@ return now -def cli_plugin_requests(config): +def cli_plugin_requests(config): # pylint: disable=too-many-branches """ Figure out which plugins the user requested with CLI and config options @@ -226,6 +266,7 @@ 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") @@ -238,6 +279,34 @@ 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_cloudxns: + req_auth = set_configurator(req_auth, "dns-cloudxns") + 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 diff -Nru python-certbot-0.10.2/certbot/plugins/selection_test.py python-certbot-0.28.0/certbot/plugins/selection_test.py --- python-certbot-0.10.2/certbot/plugins/selection_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/selection_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,13 +1,18 @@ -"""Tests for letsenecrypt.plugins.selection""" +"""Tests for letsencrypt.plugins.selection""" +import os import sys import unittest import mock import zope.component +from certbot import errors +from certbot import interfaces + +from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module from certbot.display import util as display_util +from certbot.plugins.disco import PluginsRegistry from certbot.tests import util as test_util -from certbot import interfaces class ConveniencePickPluginTest(unittest.TestCase): @@ -46,7 +51,7 @@ self.default = None self.reg = mock.MagicMock() self.question = "Question?" - self.ifaces = [] + self.ifaces = [] # type: List[interfaces.IPlugin] def _call(self): from certbot.plugins.selection import pick_plugin @@ -115,6 +120,7 @@ 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" @@ -137,15 +143,78 @@ @test_util.patch_get_utility("certbot.plugins.selection.z_util") def test_more_info(self, mock_util): mock_util().menu.side_effect = [ - (display_util.HELP, 0), - (display_util.HELP, 1), (display_util.OK, 1), ] self.assertEqual(self.mock_stand, self._call()) - self.assertEqual(mock_util().notification.call_count, 2) @test_util.patch_get_utility("certbot.plugins.selection.z_util") def test_no_choice(self, mock_util): mock_util().menu.return_value = (display_util.CANCEL, 0) self.assertTrue(self._call() is None) + + @test_util.patch_get_utility("certbot.plugins.selection.z_util") + def test_new_interaction_avoidance(self, mock_util): + mock_nginx = mock.Mock( + description_with_name="n", misconfigured=False) + mock_nginx.init().more_info.return_value = "nginx plugin" + mock_nginx.name = "nginx" + self.plugins[1] = mock_nginx + mock_util().menu.return_value = (display_util.CANCEL, 0) + + unset_cb_auto = os.environ.get("CERTBOT_AUTO") is None + if unset_cb_auto: + os.environ["CERTBOT_AUTO"] = "foo" + try: + self._call() + finally: + if unset_cb_auto: + del os.environ["CERTBOT_AUTO"] + + self.assertTrue("default" in mock_util().menu.call_args[1]) + +class GetUnpreparedInstallerTest(test_util.ConfigTestCase): + """Tests for certbot.plugins.selection.get_unprepared_installer.""" + + def setUp(self): + super(GetUnpreparedInstallerTest, self).setUp() + self.mock_apache_fail_ep = mock.Mock( + description_with_name="afail") + self.mock_apache_fail_ep.name = "afail" + self.mock_apache_ep = mock.Mock( + description_with_name="apache") + self.mock_apache_ep.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.plugins.selection import get_unprepared_installer + return get_unprepared_installer(self.config, self.plugins) + + def test_no_installer_defined(self): + self.config.configurator = None + self.assertEquals(self._call(), None) + + def test_no_available_installers(self): + self.config.configurator = "apache" + self.plugins = PluginsRegistry({}) + self.assertRaises(errors.PluginSelectionError, self._call) + + def test_get_plugin(self): + self.config.configurator = "apache" + installer = self._call() + self.assertTrue(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.name = "apache" + self.assertRaises(errors.PluginSelectionError, self._call) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff -Nru python-certbot-0.10.2/certbot/plugins/standalone.py python-certbot-0.28.0/certbot/plugins/standalone.py --- python-certbot-0.10.2/certbot/plugins/standalone.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/standalone.py 2018-11-07 21:14:56.000000000 +0000 @@ -3,8 +3,8 @@ import collections import logging import socket -import sys -import threading +# https://github.com/python/typeshed/blob/master/stdlib/2and3/socket.pyi +from socket import errno as socket_errors # type: ignore import OpenSSL import six @@ -12,16 +12,22 @@ from acme import challenges from acme import standalone as acme_standalone +# pylint: disable=unused-import, no-name-in-module +from acme.magic_typing import DefaultDict, Dict, Set, Tuple, List, Type, TYPE_CHECKING -from certbot import cli +from certbot import achallenges # pylint: disable=unused-import from certbot import errors from certbot import interfaces from certbot.plugins import common -from certbot.plugins import util logger = logging.getLogger(__name__) +if TYPE_CHECKING: + ServedType = DefaultDict[ + acme_standalone.BaseDualNetworkedServers, + Set[achallenges.KeyAuthorizationAnnotatedChallenge] + ] class ServerManager(object): """Standalone servers manager. @@ -36,14 +42,12 @@ will serve the same URLs! """ - _Instance = collections.namedtuple("_Instance", "server thread") - def __init__(self, certs, http_01_resources): - self._instances = {} + self._instances = {} # type: Dict[int, acme_standalone.BaseDualNetworkedServers] self.certs = certs self.http_01_resources = http_01_resources - def run(self, port, challenge_type): + def run(self, port, challenge_type, listenaddr=""): """Run ACME server on specified ``port``. This method is idempotent, i.e. all calls with the same pair of @@ -52,35 +56,35 @@ :param int port: Port to run the server on. :param challenge_type: Subclass of `acme.challenges.Challenge`, either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`. + :param str listenaddr: (optional) The address to listen on. Defaults to all addrs. - :returns: Server instance. + :returns: DualNetworkedServers instance. :rtype: ACMEServerMixin """ assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01) if port in self._instances: - return self._instances[port].server + return self._instances[port] - address = ("", port) + address = (listenaddr, port) try: if challenge_type is challenges.TLSSNI01: - server = acme_standalone.TLSSNI01Server(address, self.certs) + servers = acme_standalone.TLSSNI01DualNetworkedServers( + address, self.certs) # type: acme_standalone.BaseDualNetworkedServers else: # challenges.HTTP01 - server = acme_standalone.HTTP01Server( + servers = acme_standalone.HTTP01DualNetworkedServers( address, self.http_01_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) - thread = threading.Thread( - # pylint: disable=no-member - target=server.serve_forever) - thread.start() + servers.serve_forever() # if port == 0, then random free port on OS is taken # pylint: disable=no-member - real_port = server.socket.getsockname()[1] - self._instances[real_port] = self._Instance(server, thread) - return server + # 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): """Stop ACME server running on the specified ``port``. @@ -89,13 +93,12 @@ """ instance = self._instances[port] - logger.debug("Stopping server at %s:%d...", - *instance.server.socket.getsockname()[:2]) - instance.server.shutdown() + for sockname in instance.getsocknames(): + logger.debug("Stopping server at %s:%d...", + *sockname[:2]) # Not calling server_close causes problems when renewing multiple # certs with `certbot renew` using TLSSNI01 and PyOpenSSL 0.13 - instance.server.server_close() - instance.thread.join() + instance.shutdown_and_server_close() del self._instances[port] def running(self): @@ -104,50 +107,68 @@ Once the server is stopped using `stop`, it will not be returned. - :returns: Mapping from ``port`` to ``server``. + :returns: Mapping from ``port`` to ``servers``. :rtype: tuple """ - return dict((port, instance.server) for port, instance - in six.iteritems(self._instances)) + return self._instances.copy() -SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01] +SUPPORTED_CHALLENGES = [challenges.HTTP01, challenges.TLSSNI01] \ +# type: List[Type[challenges.KeyAuthorizationChallenge]] -def supported_challenges_validator(data): - """Supported challenges validator for the `argparse`. +class SupportedChallengesAction(argparse.Action): + """Action class for parsing standalone_supported_challenges.""" - It should be passed as `type` argument to `add_argument`. + def __call__(self, parser, namespace, values, option_string=None): + logger.warning( + "The standalone specific supported challenges flag is " + "deprecated. Please use the --preferred-challenges flag " + "instead.") + converted_values = self._convert_and_validate(values) + namespace.standalone_supported_challenges = converted_values - """ - if cli.set_by_cli("standalone_supported_challenges"): - sys.stderr.write( - "WARNING: The standalone specific " - "supported challenges flag is deprecated.\n" - "Please use the --preferred-challenges flag instead.\n") - challs = data.split(",") - - # tls-sni-01 was dvsni during private beta - if "dvsni" in challs: - logger.info("Updating legacy standalone_supported_challenges value") - challs = [challenges.TLSSNI01.typ if chall == "dvsni" else chall - for chall in challs] - data = ",".join(challs) - - unrecognized = [name for name in challs - if name not in challenges.Challenge.TYPES] - if unrecognized: - raise argparse.ArgumentTypeError( - "Unrecognized challenges: {0}".format(", ".join(unrecognized))) - - choices = set(chall.typ for chall in SUPPORTED_CHALLENGES) - if not set(challs).issubset(choices): - raise argparse.ArgumentTypeError( - "Plugin does not support the following (valid) " - "challenges: {0}".format(", ".join(set(challs) - choices))) + def _convert_and_validate(self, data): + """Validate the value of supported challenges provided by the user. + + References to "dvsni" are automatically converted to "tls-sni-01". - return data + :param str data: comma delimited list of challenge types + + :returns: validated and converted list of challenge types + :rtype: str + + """ + challs = data.split(",") + + # tls-sni-01 was dvsni during private beta + if "dvsni" in challs: + logger.info( + "Updating legacy standalone_supported_challenges value") + challs = [challenges.TLSSNI01.typ if chall == "dvsni" else chall + for chall in challs] + data = ",".join(challs) + + unrecognized = [name for name in challs + if name not in challenges.Challenge.TYPES] + + # argparse.ArgumentErrors raised out of argparse.Action objects + # are caught by argparse which prints usage information and the + # error that occurred before calling sys.exit. + if unrecognized: + raise argparse.ArgumentError( + self, + "Unrecognized challenges: {0}".format(", ".join(unrecognized))) + + choices = set(chall.typ for chall in SUPPORTED_CHALLENGES) + if not set(challs).issubset(choices): + raise argparse.ArgumentError( + self, + "Plugin does not support the following (valid) " + "challenges: {0}".format(", ".join(set(challs) - choices))) + + return data @zope.interface.implementer(interfaces.IAuthenticator) @@ -170,14 +191,15 @@ self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) - self.served = collections.defaultdict(set) + self.served = collections.defaultdict(set) # type: ServedType # 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 = {} - self.http_01_resources = set() + self.certs = {} # type: Dict[bytes, Tuple[OpenSSL.crypto.PKey, OpenSSL.crypto.X509]] + self.http_01_resources = set() \ + # type: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] self.servers = ServerManager(self.certs, self.http_01_resources) @@ -185,7 +207,7 @@ def add_parser_arguments(cls, add): add("supported-challenges", help=argparse.SUPPRESS, - type=supported_challenges_validator, + action=SupportedChallengesAction, default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES)) @property @@ -208,81 +230,70 @@ # pylint: disable=unused-argument,missing-docstring return self.supported_challenges - def _verify_ports_are_available(self, achalls): - """Confirm the ports are available to solve all achalls. - - :param list achalls: list of - :class:`~certbot.achallenges.AnnotatedChallenge` - - :raises .errors.MisconfigurationError: if required port is - unavailable - - """ - ports = [] - if any(isinstance(ac.chall, challenges.HTTP01) for ac in achalls): - ports.append(self.config.http01_port) - if any(isinstance(ac.chall, challenges.TLSSNI01) for ac in achalls): - ports.append(self.config.tls_sni_01_port) - - renewer = (self.config.verb == "renew") - - if any(util.already_listening(port, renewer) for port in ports): - raise errors.MisconfigurationError( - "At least one of the required ports is already taken.") - def perform(self, achalls): # pylint: disable=missing-docstring - self._verify_ports_are_available(achalls) + return [self._try_perform_single(achall) for achall in achalls] - try: - return self.perform2(achalls) - except errors.StandaloneBindError as error: - display = zope.component.getUtility(interfaces.IDisplay) - - if error.socket_error.errno == socket.errno.EACCES: - display.notification( - "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), force_interactive=True) - elif error.socket_error.errno == socket.errno.EADDRINUSE: - display.notification( - "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), force_interactive=True) - else: - raise # XXX: How to handle unknown errors in binding? - - def perform2(self, achalls): - """Perform achallenges without IDisplay interaction.""" - responses = [] - - for achall in achalls: - if isinstance(achall.chall, challenges.HTTP01): - server = self.servers.run( - self.config.http01_port, challenges.HTTP01) - response, validation = achall.response_and_validation() - self.http_01_resources.add( - acme_standalone.HTTP01RequestHandler.HTTP01Resource( - chall=achall.chall, response=response, - validation=validation)) - else: # tls-sni-01 - server = self.servers.run( - self.config.tls_sni_01_port, challenges.TLSSNI01) - response, (cert, _) = achall.response_and_validation( - cert_key=self.key) - self.certs[response.z_domain] = (self.key, cert) - self.served[server].add(achall) - responses.append(response) - - return responses + def _try_perform_single(self, achall): + while True: + try: + return self._perform_single(achall) + except errors.StandaloneBindError as error: + _handle_perform_error(error) + + def _perform_single(self, achall): + if isinstance(achall.chall, challenges.HTTP01): + servers, response = self._perform_http_01(achall) + else: # tls-sni-01 + servers, response = self._perform_tls_sni_01(achall) + self.served[servers].add(achall) + return response + + def _perform_http_01(self, achall): + 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 _perform_tls_sni_01(self, achall): + port = self.config.tls_sni_01_port + addr = self.config.tls_sni_01_address + servers = self.servers.run(port, challenges.TLSSNI01, listenaddr=addr) + response, (cert, _) = achall.response_and_validation(cert_key=self.key) + self.certs[response.z_domain] = (self.key, cert) + return servers, response def cleanup(self, achalls): # pylint: disable=missing-docstring - # reduce self.served and close servers if none challenges are served - for server, server_achalls in self.served.items(): + # 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, server in six.iteritems(self.servers.running()): - if not self.served[server]: + for port, servers in six.iteritems(self.servers.running()): + if not self.served[servers]: self.servers.stop(port) + + +def _handle_perform_error(error): + if error.socket_error.errno == socket_errors.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)) + elif error.socket_error.errno == socket_errors.EADDRINUSE: + display = zope.component.getUtility(interfaces.IDisplay) + 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.yesno(msg, "Retry", + "Cancel", default=False) + if not should_retry: + raise errors.PluginError(msg) + else: + raise diff -Nru python-certbot-0.10.2/certbot/plugins/standalone_test.py python-certbot-0.28.0/certbot/plugins/standalone_test.py --- python-certbot-0.10.2/certbot/plugins/standalone_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/standalone_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -2,17 +2,21 @@ import argparse import socket import unittest +# https://github.com/python/typeshed/blob/master/stdlib/2and3/socket.pyi +from socket import errno as socket_errors # type: ignore +import josepy as jose import mock import six +import OpenSSL.crypto # pylint: disable=unused-import + from acme import challenges -from acme import jose -from acme import standalone as acme_standalone +from acme import standalone as acme_standalone # pylint: disable=unused-import +from acme.magic_typing import Dict, Tuple, Set # pylint: disable=unused-import, no-name-in-module from certbot import achallenges from certbot import errors -from certbot import interfaces from certbot.tests import acme_util from certbot.tests import util as test_util @@ -23,8 +27,9 @@ def setUp(self): from certbot.plugins.standalone import ServerManager - self.certs = {} - self.http_01_resources = {} + self.certs = {} # type: Dict[bytes, Tuple[OpenSSL.crypto.PKey, OpenSSL.crypto.X509]] + self.http_01_resources = {} \ + # type: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] self.mgr = ServerManager(self.certs, self.http_01_resources) def test_init(self): @@ -34,7 +39,7 @@ def _test_run_stop(self, challenge_type): server = self.mgr.run(port=0, challenge_type=challenge_type) - port = server.socket.getsockname()[1] # pylint: disable=no-member + port = server.getsocknames()[0][1] # pylint: disable=no-member self.assertEqual(self.mgr.running(), {port: server}) self.mgr.stop(port=port) self.assertEqual(self.mgr.running(), {}) @@ -47,7 +52,7 @@ def test_run_idempotent(self): server = self.mgr.run(port=0, challenge_type=challenges.HTTP01) - port = server.socket.getsockname()[1] # pylint: disable=no-member + port = server.getsocknames()[0][1] # pylint: disable=no-member server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {port: server}) self.assertTrue(server is server2) @@ -55,37 +60,40 @@ self.assertEqual(self.mgr.running(), {}) def test_run_bind_error(self): - some_server = socket.socket() + 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 self.assertRaises( errors.StandaloneBindError, self.mgr.run, port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {}) -class SupportedChallengesValidatorTest(unittest.TestCase): - """Tests for plugins.standalone.supported_challenges_validator.""" +class SupportedChallengesActionTest(unittest.TestCase): + """Tests for plugins.standalone.SupportedChallengesAction.""" + + def _call(self, value): + with mock.patch("certbot.plugins.standalone.logger") as mock_logger: + # stderr is mocked to prevent potential argparse error + # output from cluttering test output + with mock.patch("sys.stderr"): + config = self.parser.parse_args([self.flag, value]) + + self.assertTrue(mock_logger.warning.called) + return getattr(config, self.dest) def setUp(self): - self.set_by_cli_patch = mock.patch( - "certbot.plugins.standalone.cli.set_by_cli") - self.stderr_patch = mock.patch("certbot.plugins.standalone.sys.stderr") - - self.set_by_cli_patch.start().return_value = True - self.stderr = self.stderr_patch.start() - - def tearDown(self): - self.set_by_cli_patch.stop() - self.stderr_patch.stop() - - def _call(self, data): - from certbot.plugins.standalone import ( - supported_challenges_validator) - return_value = supported_challenges_validator(data) - self.assertTrue(self.stderr.write.called) # pylint: disable=no-member - self.stderr.write.reset_mock() # pylint: disable=no-member - return return_value + self.flag = "--standalone-supported-challenges" + self.dest = self.flag[2:].replace("-", "_") + self.parser = argparse.ArgumentParser() + + from certbot.plugins.standalone import SupportedChallengesAction + self.parser.add_argument(self.flag, action=SupportedChallengesAction) def test_correct(self): self.assertEqual("tls-sni-01", self._call("tls-sni-01")) @@ -95,10 +103,10 @@ def test_unrecognized(self): assert "foo" not in challenges.Challenge.TYPES - self.assertRaises(argparse.ArgumentTypeError, self._call, "foo") + self.assertRaises(SystemExit, self._call, "foo") def test_not_subset(self): - self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") + self.assertRaises(SystemExit, self._call, "dns") def test_dvsni(self): self.assertEqual("tls-sni-01", self._call("dvsni")) @@ -114,6 +122,7 @@ open_socket.close() return port + class AuthenticatorTest(unittest.TestCase): """Tests for certbot.plugins.standalone.Authenticator.""" @@ -124,6 +133,7 @@ tls_sni_01_port=get_open_port(), http01_port=get_open_port(), standalone_supported_challenges="tls-sni-01,http-01") self.auth = Authenticator(self.config, name="standalone") + self.auth.servers = mock.MagicMock() def test_supported_challenges(self): self.assertEqual(self.auth.supported_challenges, @@ -146,95 +156,66 @@ self.assertEqual(self.auth.get_chall_pref(domain=None), [challenges.TLSSNI01]) - @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) - tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.TLSSNI01_P, domain=domain, account_key=key) - - return [http_01, tls_sni_01] + def test_perform(self): + achalls = self._get_achalls() + response = self.auth.perform(achalls) - @mock.patch("certbot.plugins.standalone.util") - def test_perform_already_listening(self, mock_util): - http_01, tls_sni_01 = self._get_achalls() - - for achall, port in ((http_01, self.config.http01_port,), - (tls_sni_01, self.config.tls_sni_01_port)): - mock_util.already_listening.return_value = True - self.assertRaises( - errors.MisconfigurationError, self.auth.perform, [achall]) - mock_util.already_listening.assert_called_once_with(port, False) - mock_util.already_listening.reset_mock() + expected = [achall.response(achall.account_key) for achall in achalls] + self.assertEqual(response, expected) @test_util.patch_get_utility() - def test_perform(self, unused_mock_get_utility): - achalls = self._get_achalls() + def test_perform_eaddrinuse_retry(self, mock_get_utility): + mock_utility = mock_get_utility() + errno = socket_errors.EADDRINUSE + error = errors.StandaloneBindError(mock.MagicMock(errno=errno), -1) + self.auth.servers.run.side_effect = [error] + 2 * [mock.MagicMock()] + mock_yesno = mock_utility.yesno + mock_yesno.return_value = True - self.auth.perform2 = mock.Mock(return_value=mock.sentinel.responses) - self.assertEqual(mock.sentinel.responses, self.auth.perform(achalls)) - self.auth.perform2.assert_called_once_with(achalls) + self.test_perform() + self._assert_correct_yesno_call(mock_yesno) @test_util.patch_get_utility() - def _test_perform_bind_errors(self, errno, achalls, mock_get_utility): - port = get_open_port() - def _perform2(unused_achalls): - raise errors.StandaloneBindError(mock.Mock(errno=errno), port) - - self.auth.perform2 = mock.MagicMock(side_effect=_perform2) - self.auth.perform(achalls) - mock_get_utility.assert_called_once_with(interfaces.IDisplay) - notification = mock_get_utility.return_value.notification - self.assertEqual(1, notification.call_count) - self.assertTrue(str(port) in notification.call_args[0][0]) + 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 + + errno = socket_errors.EADDRINUSE + self.assertRaises(errors.PluginError, self._fail_perform, errno) + self._assert_correct_yesno_call(mock_yesno) + + def _assert_correct_yesno_call(self, mock_yesno): + yesno_args, yesno_kwargs = mock_yesno.call_args + self.assertTrue("in use" in yesno_args[0]) + self.assertFalse(yesno_kwargs.get("default", True)) def test_perform_eacces(self): - # pylint: disable=no-value-for-parameter - self._test_perform_bind_errors(socket.errno.EACCES, []) + errno = socket_errors.EACCES + self.assertRaises(errors.PluginError, self._fail_perform, errno) - def test_perform_eaddrinuse(self): - # pylint: disable=no-value-for-parameter - self._test_perform_bind_errors(socket.errno.EADDRINUSE, []) - - def test_perfom_unknown_bind_error(self): + def test_perform_unexpected_socket_error(self): + errno = socket_errors.ENOTCONN self.assertRaises( - errors.StandaloneBindError, self._test_perform_bind_errors, - socket.errno.ENOTCONN, []) - - def test_perform2(self): - http_01, tls_sni_01 = self._get_achalls() + errors.StandaloneBindError, self._fail_perform, errno) - self.auth.servers = mock.MagicMock() - - def _run(port, tls): # pylint: disable=unused-argument - return "server{0}".format(port) + def _fail_perform(self, errno): + error = errors.StandaloneBindError(mock.MagicMock(errno=errno), -1) + self.auth.servers.run.side_effect = error + self.auth.perform(self._get_achalls()) - self.auth.servers.run.side_effect = _run - responses = self.auth.perform2([http_01, tls_sni_01]) + @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) + tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.TLSSNI01_P, domain=domain, account_key=key) - self.assertTrue(isinstance(responses, list)) - self.assertEqual(2, len(responses)) - self.assertTrue(isinstance(responses[0], challenges.HTTP01Response)) - self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response)) - - self.assertEqual(self.auth.servers.run.mock_calls, [ - mock.call(self.config.http01_port, challenges.HTTP01), - mock.call(self.config.tls_sni_01_port, challenges.TLSSNI01), - ]) - self.assertEqual(self.auth.served, { - "server" + str(self.config.tls_sni_01_port): set([tls_sni_01]), - "server" + str(self.config.http01_port): set([http_01]), - }) - self.assertEqual(1, len(self.auth.http_01_resources)) - self.assertEqual(1, len(self.auth.certs)) - self.assertEqual(list(self.auth.http_01_resources), [ - acme_standalone.HTTP01RequestHandler.HTTP01Resource( - acme_util.HTTP01, responses[0], mock.ANY)]) + return [http_01, tls_sni_01] def test_cleanup(self): - self.auth.servers = mock.Mock() self.auth.servers.running.return_value = { 1: "server1", 2: "server2", diff -Nru python-certbot-0.10.2/certbot/plugins/storage.py python-certbot-0.28.0/certbot/plugins/storage.py --- python-certbot-0.10.2/certbot/plugins/storage.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/storage.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,119 @@ +"""Plugin storage class.""" +import json +import logging +import os + +from acme.magic_typing import Any, Dict # pylint: disable=unused-import, no-name-in-module +from certbot import errors + +logger = logging.getLogger(__name__) + +class PluginStorage(object): + """Class implementing storage functionality for plugins""" + + def __init__(self, config, classkey): + """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 = None + self._storagepath = None + + def _initialize_storage(self): + """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): + """Reads PluginStorage content from the disk to a dict structure + + :raises .errors.PluginStorageError: when unable to open or read the file + """ + data = dict() # type: 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): + """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(os.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, value): + """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 not self._classkey in self._data.keys(): + self._data[self._classkey] = dict() + self._data[self._classkey][key] = value + + def fetch(self, key): + """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] diff -Nru python-certbot-0.10.2/certbot/plugins/storage_test.py python-certbot-0.28.0/certbot/plugins/storage_test.py --- python-certbot-0.10.2/certbot/plugins/storage_test.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/storage_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,117 @@ +"""Tests for certbot.plugins.storage.PluginStorage""" +import json +import mock +import os +import unittest + +from certbot import errors + +from certbot.plugins import common +from certbot.tests import util as test_util + +class PluginStorageTest(test_util.ConfigTestCase): + """Test for certbot.plugins.storage.PluginStorage""" + + def setUp(self): + super(PluginStorageTest, self).setUp() + self.plugin_cls = common.Installer + os.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("six.moves.builtins.open", mock_open): + with mock.patch('os.path.isfile', return_value=True): + with mock.patch("certbot.reverter.util"): + self.assertRaises(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") + self.assertRaises(KeyError, + nocontent.storage.fetch, "value") + self.assertTrue(mock_log.called) + self.assertTrue("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") + self.assertRaises(errors.PluginError, + corrupted.storage.fetch, + "value") + self.assertTrue("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 + self.assertRaises(errors.PluginStorageError, + self.plugin.storage.save) + self.assertTrue("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("os.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.assertRaises(errors.PluginStorageError, + self.plugin.storage.save) + self.assertTrue("Could not write" in mock_log.call_args[0][0]) + + def test_save_uninitialized(self): + with mock.patch("certbot.reverter.util"): + self.assertRaises(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") + self.assertRaises(KeyError, + plugin2.storage.fetch, "first_key") + self.assertRaises(KeyError, + plugin2.storage.fetch, "first") + self.assertEqual(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") + self.assertEqual(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) + self.assertTrue("mockplugin" in psjson.keys()) + self.assertEqual(len(psjson), 1) + self.assertEqual(psjson["mockplugin"]["testkey"], "testvalue") + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff -Nru python-certbot-0.10.2/certbot/plugins/util.py python-certbot-0.28.0/certbot/plugins/util.py --- python-certbot-0.10.2/certbot/plugins/util.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/util.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,34 +1,30 @@ """Plugin utilities.""" import logging import os -import socket -import zope.component - -from acme import errors as acme_errors -from acme import util as acme_util - -from certbot import interfaces from certbot import util -PSUTIL_REQUIREMENT = "psutil>=2.2.1" - -try: - acme_util.activate(PSUTIL_REQUIREMENT) - import psutil # pragma: no cover - USE_PSUTIL = True -except acme_errors.DependencyError: # pragma: no cover - USE_PSUTIL = False - logger = logging.getLogger(__name__) -RENEWER_EXTRA_MSG = ( - " For automated renewal, you may want to use a script that stops" - " and starts your webserver. You can find an example at" - " https://certbot.eff.org/docs/using.html#renewal ." - " Alternatively you can use the webroot plugin to renew without" - " needing to stop and start your webserver.") +def get_prefixes(path): + """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 = [] + while len(prefix) > 0: + 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): """Attempt to perform PATH surgery to find cmd @@ -56,108 +52,6 @@ return True else: expanded = " expanded" if any(added) else "" - logger.warning("Failed to find %s in%s PATH: %s", cmd, - expanded, path) - return False - - -def already_listening(port, renewer=False): - """Check if a process is already listening on the port. - - If so, also tell the user via a display notification. - - .. warning:: - On some operating systems, this function can only usefully be - run as root. - - :param int port: The TCP port in question. - :returns: True or False. - - """ - - if USE_PSUTIL: - return already_listening_psutil(port, renewer=renewer) - else: - logger.debug("Psutil not found, using simple socket check.") - return already_listening_socket(port, renewer=renewer) - - -def already_listening_socket(port, renewer=False): - """Simple socket based check to find out if port is already in use - - :param int port: The TCP port in question. - :returns: True or False - """ - - try: - testsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) - testsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - testsocket.bind(("", port)) - except socket.error: - display = zope.component.getUtility(interfaces.IDisplay) - extra = "" - if renewer: - extra = RENEWER_EXTRA_MSG - display.notification( - "Port {0} is already in use by another process. This will " - "prevent us from binding to that port. Please stop the " - "process that is populating the port in question and try " - "again. {1}".format(port, extra), force_interactive=True) - return True - finally: - testsocket.close() - except socket.error: - pass - return False - - -def already_listening_psutil(port, renewer=False): - """Psutil variant of the open port check - - :param int port: The TCP port in question. - :returns: True or False. - - """ - try: - net_connections = psutil.net_connections() - except psutil.AccessDenied as error: - logger.info("Access denied when trying to list network " - "connections: %s. Are you root?", error) - # this function is just a pre-check that often causes false - # positives and problems in testing (c.f. #680 on Mac, #255 - # generally); we will fail later in bind() anyway + logger.debug("Failed to find executable %s in%s PATH: %s", cmd, + expanded, path) return False - - listeners = [conn.pid for conn in net_connections - if conn.status == 'LISTEN' and - conn.type == socket.SOCK_STREAM and - conn.laddr[1] == port] - try: - if listeners and listeners[0] is not None: - # conn.pid may be None if the current process doesn't have - # permission to identify the listening process! Additionally, - # listeners may have more than one element if separate - # sockets have bound the same port on separate interfaces. - # We currently only have UI to notify the user about one - # of them at a time. - pid = listeners[0] - name = psutil.Process(pid).name() - display = zope.component.getUtility(interfaces.IDisplay) - extra = "" - if renewer: - extra = RENEWER_EXTRA_MSG - display.notification( - "The program {0} (process ID {1}) is already listening " - "on TCP port {2}. This will prevent us from binding to " - "that port. Please stop the {0} program temporarily " - "and then try again.{3}".format(name, pid, port, extra), - force_interactive=True) - return True - except (psutil.NoSuchProcess, psutil.AccessDenied): - # Perhaps the result of a race where the process could have - # exited or relinquished the port (NoSuchProcess), or the result - # of an OS policy where we're not allowed to look up the process - # name (AccessDenied). - pass - return False diff -Nru python-certbot-0.10.2/certbot/plugins/util_test.py python-certbot-0.28.0/certbot/plugins/util_test.py --- python-certbot-0.10.2/certbot/plugins/util_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/util_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,20 +1,24 @@ """Tests for certbot.plugins.util.""" import os -import socket import unittest import mock -from certbot.plugins.util import PSUTIL_REQUIREMENT -from certbot.tests import util as test_util - +class GetPrefixTest(unittest.TestCase): + """Tests for certbot.plugins.get_prefixes.""" + def test_get_prefix(self): + from certbot.plugins.util import get_prefixes + self.assertEqual( + get_prefixes('/a/b/c'), + [os.path.normpath(path) for path in ['/a/b/c', '/a/b', '/a', '/']]) + self.assertEqual(get_prefixes('/'), [os.path.normpath('/')]) + self.assertEqual(get_prefixes('a'), ['a']) class PathSurgeryTest(unittest.TestCase): """Tests for certbot.plugins.path_surgery.""" - @mock.patch("certbot.plugins.util.logger.warning") @mock.patch("certbot.plugins.util.logger.debug") - def test_path_surgery(self, mock_debug, mock_warn): + def test_path_surgery(self, 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): @@ -22,152 +26,15 @@ mock_exists.return_value = True self.assertEqual(path_surgery("eg"), True) self.assertEqual(mock_debug.call_count, 0) - self.assertEqual(mock_warn.call_count, 0) self.assertEqual(os.environ["PATH"], all_path["PATH"]) no_path = {"PATH": "/tmp/"} with mock.patch.dict('os.environ', no_path): path_surgery("thingy") - self.assertEqual(mock_debug.call_count, 1) - self.assertEqual(mock_warn.call_count, 1) - self.assertTrue("Failed to find" in mock_warn.call_args[0][0]) + self.assertEqual(mock_debug.call_count, 2) + self.assertTrue("Failed to find" in mock_debug.call_args[0][0]) self.assertTrue("/usr/local/bin" in os.environ["PATH"]) self.assertTrue("/tmp" in os.environ["PATH"]) -class AlreadyListeningTest(unittest.TestCase): - """Tests for certbot.plugins.already_listening.""" - @classmethod - def _call(cls, *args, **kwargs): - from certbot.plugins.util import already_listening - return already_listening(*args, **kwargs) - - -class AlreadyListeningTestNoPsutil(AlreadyListeningTest): - """Tests for certbot.plugins.already_listening when - psutil is not available""" - @classmethod - def _call(cls, *args, **kwargs): - with mock.patch("certbot.plugins.util.USE_PSUTIL", False): - return super( - AlreadyListeningTestNoPsutil, cls)._call(*args, **kwargs) - - @test_util.patch_get_utility() - def test_ports_available(self, mock_getutil): - # Ensure we don't get error - with mock.patch("socket.socket.bind"): - self.assertFalse(self._call(80)) - self.assertFalse(self._call(80, True)) - self.assertEqual(mock_getutil.call_count, 0) - - @test_util.patch_get_utility() - def test_ports_blocked(self, mock_getutil): - with mock.patch("certbot.plugins.util.socket.socket.bind") as mock_bind: - mock_bind.side_effect = socket.error - self.assertTrue(self._call(80)) - self.assertTrue(self._call(80, True)) - with mock.patch("certbot.plugins.util.socket.socket") as mock_socket: - mock_socket.side_effect = socket.error - self.assertFalse(self._call(80)) - self.assertEqual(mock_getutil.call_count, 2) - - -@test_util.skip_unless(test_util.requirement_available(PSUTIL_REQUIREMENT), - "optional dependency psutil is not available") -class AlreadyListeningTestPsutil(AlreadyListeningTest): - """Tests for certbot.plugins.already_listening.""" - @mock.patch("certbot.plugins.util.psutil.net_connections") - @mock.patch("certbot.plugins.util.psutil.Process") - @test_util.patch_get_utility() - def test_race_condition(self, mock_get_utility, mock_process, mock_net): - # This tests a race condition, or permission problem, or OS - # incompatibility in which, for some reason, no process name can be - # found to match the identified listening PID. - import psutil - from psutil._common import sconn - conns = [ - sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), - raddr=(), status="LISTEN", pid=None), - sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), - raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), - sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), - raddr=("::1", 111), status="CLOSE_WAIT", pid=None), - sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17), - raddr=(), status="LISTEN", pid=4416)] - mock_net.return_value = conns - mock_process.side_effect = psutil.NoSuchProcess("No such PID") - # We simulate being unable to find the process name of PID 4416, - # which results in returning False. - self.assertFalse(self._call(17)) - self.assertEqual(mock_get_utility.generic_notification.call_count, 0) - mock_process.assert_called_once_with(4416) - - @mock.patch("certbot.plugins.util.psutil.net_connections") - @mock.patch("certbot.plugins.util.psutil.Process") - @test_util.patch_get_utility() - def test_not_listening(self, mock_get_utility, mock_process, mock_net): - from psutil._common import sconn - conns = [ - sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), - raddr=(), status="LISTEN", pid=None), - sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), - raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), - sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), - raddr=("::1", 111), status="CLOSE_WAIT", pid=None)] - mock_net.return_value = conns - mock_process.name.return_value = "inetd" - self.assertFalse(self._call(17)) - self.assertEqual(mock_get_utility.generic_notification.call_count, 0) - self.assertEqual(mock_process.call_count, 0) - - @mock.patch("certbot.plugins.util.psutil.net_connections") - @mock.patch("certbot.plugins.util.psutil.Process") - @test_util.patch_get_utility() - def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net): - from psutil._common import sconn - conns = [ - sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), - raddr=(), status="LISTEN", pid=None), - sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), - raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), - sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), - raddr=("::1", 111), status="CLOSE_WAIT", pid=None), - sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17), - raddr=(), status="LISTEN", pid=4416)] - mock_net.return_value = conns - mock_process.name.return_value = "inetd" - result = self._call(17, True) - self.assertTrue(result) - self.assertEqual(mock_get_utility.call_count, 1) - mock_process.assert_called_once_with(4416) - - @mock.patch("certbot.plugins.util.psutil.net_connections") - @mock.patch("certbot.plugins.util.psutil.Process") - @test_util.patch_get_utility() - def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net): - from psutil._common import sconn - conns = [ - sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), - raddr=(), status="LISTEN", pid=None), - sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), - raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), - sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), - raddr=("::1", 111), status="CLOSE_WAIT", pid=None), - sconn(fd=3, family=10, type=1, laddr=("::", 12345), raddr=(), - status="LISTEN", pid=4420), - sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17), - raddr=(), status="LISTEN", pid=4416)] - mock_net.return_value = conns - mock_process.name.return_value = "inetd" - result = self._call(12345) - self.assertTrue(result) - self.assertEqual(mock_get_utility.call_count, 1) - mock_process.assert_called_once_with(4420) - - @mock.patch("certbot.plugins.util.psutil.net_connections") - def test_access_denied_exception(self, mock_net): - import psutil - mock_net.side_effect = psutil.AccessDenied("") - self.assertFalse(self._call(12345)) - if __name__ == "__main__": unittest.main() # pragma: no cover diff -Nru python-certbot-0.10.2/certbot/plugins/webroot.py python-certbot-0.28.0/certbot/plugins/webroot.py --- python-certbot-0.10.2/certbot/plugins/webroot.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/webroot.py 2018-11-07 21:14:56.000000000 +0000 @@ -10,13 +10,19 @@ import zope.component import zope.interface -from acme import challenges +from acme import challenges # pylint: disable=unused-import +# pylint: disable=unused-import, no-name-in-module +from acme.magic_typing import Dict, Set, DefaultDict, List +# pylint: enable=unused-import, no-name-in-module +from certbot import achallenges # pylint: disable=unused-import from certbot import cli from certbot import errors from certbot import interfaces from certbot.display import util as display_util +from certbot.display import ops from certbot.plugins import common +from certbot.plugins import util logger = logging.getLogger(__name__) @@ -62,8 +68,11 @@ def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - self.full_roots = {} - self.performed = collections.defaultdict(set) + self.full_roots = {} # type: Dict[str, str] + self.performed = collections.defaultdict(set) \ + # type: DefaultDict[str, Set[achallenges.KeyAuthorizationAnnotatedChallenge]] + # stack of dirs successfully created by this authenticator + self._created_dirs = [] # type: List[str] def prepare(self): # pylint: disable=missing-docstring pass @@ -101,10 +110,14 @@ webroot = None while webroot is None: - webroot = self._prompt_with_webroot_list(domain, known_webroots) - - if webroot is None: - webroot = self._prompt_for_new_webroot(domain) + 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 @@ -116,42 +129,28 @@ code, index = display.menu( "Select the webroot for {0}:".format(domain), ["Enter a new webroot"] + known_webroots, - help_label="Help", cli_flag=path_flag, force_interactive=True) + 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.") - elif code == display_util.HELP: - display.notification( - "To use the webroot plugin, you need to have an " - "HTTP server running on this system serving files " - "for the requested domain. Additionally, this " - "server should be serving all files contained in a " - "public_html or webroot directory. The webroot " - "plugin works by temporarily saving necessary " - "resources in the HTTP server's webroot directory " - "to pass domain validation challenges.", - force_interactive=True) else: # code == display_util.OK return None if index == 0 else known_webroots[index - 1] - def _prompt_for_new_webroot(self, domain): - display = zope.component.getUtility(interfaces.IDisplay) - - while True: - code, webroot = display.directory_select( - "Input the webroot for {0}:".format(domain), - force_interactive=True) - if code == display_util.HELP: - # Displaying help is not currently implemented + def _prompt_for_new_webroot(self, domain, allowraise=False): + 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 - elif code == display_util.CANCEL: - return None - else: # code == display_util.OK - try: - return _validate_webroot(webroot) - except errors.PluginError as error: - display.notification(str(error), pause=False) + else: + raise errors.PluginError( + "Every requested domain must have a " + "webroot when using the webroot plugin.") + else: # code == display_util.OK + return _validate_webroot(webroot) def _create_challenge_dirs(self): path_map = self.conf("map") @@ -162,7 +161,6 @@ " --help webroot for examples.") for name, path in path_map.items(): self.full_roots[name] = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH) - logger.debug("Creating root challenges validation dir at %s", self.full_roots[name]) @@ -170,27 +168,28 @@ # Umask is used instead of chmod to ensure the client can also # run as non-root (GH #1795) old_umask = os.umask(0o022) - try: - # This is coupled with the "umask" call above because - # os.makedirs's "mode" parameter may not always work: - # https://stackoverflow.com/questions/5231901/permission-problems-when-creating-a-dir-with-os-makedirs-python - os.makedirs(self.full_roots[name], 0o0755) - - # Set owner as parent directory if possible - try: - stat_path = os.stat(path) - os.chown(self.full_roots[name], stat_path.st_uid, - stat_path.st_gid) - except OSError as exception: - logger.info("Unable to change owner and uid of webroot directory") - logger.debug("Error was: %s", exception) - - except OSError as exception: - if exception.errno != errno.EEXIST: - raise errors.PluginError( - "Couldn't create root for {0} http-01 " - "challenge responses: {1}", name, exception) + stat_path = os.stat(path) + # 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): + try: + # 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 + os.mkdir(prefix, 0o0755) + self._created_dirs.append(prefix) + # Set owner as parent directory if possible + try: + os.chown(prefix, stat_path.st_uid, stat_path.st_gid) + except (OSError, AttributeError) as exception: + logger.info("Unable to change owner and uid of webroot directory") + logger.debug("Error was: %s", exception) + except OSError as exception: + if exception.errno not in (errno.EEXIST, errno.EISDIR): + raise errors.PluginError( + "Couldn't create root for {0} http-01 " + "challenge responses: {1}".format(name, exception)) finally: os.umask(old_umask) @@ -214,7 +213,6 @@ os.umask(old_umask) self.performed[root_path].add(achall) - return response def cleanup(self, achalls): # pylint: disable=missing-docstring @@ -226,16 +224,17 @@ os.remove(validation_path) self.performed[root_path].remove(achall) - for root_path, achalls in six.iteritems(self.performed): - if not achalls: - try: - os.rmdir(root_path) - logger.debug("All challenges cleaned up, removing %s", - root_path) - except OSError as exc: - logger.info( - "Unable to clean up challenge directory %s", root_path) - logger.debug("Error was: %s", exc) + not_removed = [] # type: List[str] + while len(self._created_dirs) > 0: + 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): diff -Nru python-certbot-0.10.2/certbot/plugins/webroot_test.py python-certbot-0.28.0/certbot/plugins/webroot_test.py --- python-certbot-0.10.2/certbot/plugins/webroot_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/plugins/webroot_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -4,19 +4,20 @@ import argparse import errno +import json import os import shutil -import stat import tempfile import unittest +import josepy as jose import mock import six from acme import challenges -from acme import jose from certbot import achallenges +from certbot import compat from certbot import errors from certbot.display import util as display_util @@ -36,6 +37,8 @@ def setUp(self): from certbot.plugins.webroot import Authenticator self.path = tempfile.mkdtemp() + 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( @@ -50,7 +53,7 @@ def test_more_info(self): more_info = self.auth.more_info() - self.assertTrue(isinstance(more_info, str)) + self.assertTrue(isinstance(more_info, six.string_types)) self.assertTrue(self.path in more_info) def test_add_parser_arguments(self): @@ -84,10 +87,8 @@ self.config.webroot_map = {"otherthing.com": self.path} mock_display = mock_get_utility() - mock_display.menu.side_effect = ((display_util.HELP, -1), - (display_util.CANCEL, -1),) + mock_display.menu.side_effect = ((display_util.CANCEL, -1),) self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) - self.assertTrue(mock_display.notification.called) self.assertTrue(mock_display.menu.called) for call in mock_display.menu.call_args_list: self.assertTrue(self.achall.domain in call[0][0]) @@ -98,24 +99,30 @@ @test_util.patch_get_utility() def test_new_webroot(self, mock_get_utility): self.config.webroot_path = [] - self.config.webroot_map = {} - - imaginary_dir = os.path.join(os.sep, "imaginary", "dir") + self.config.webroot_map = {"something.com": self.path} mock_display = mock_get_utility() mock_display.menu.return_value = (display_util.OK, 0,) - mock_display.directory_select.side_effect = ( - (display_util.HELP, -1,), (display_util.CANCEL, -1,), - (display_util.OK, imaginary_dir,), (display_util.OK, self.path,),) - self.auth.perform([self.achall]) + with mock.patch('certbot.display.ops.validated_directory') as m: + m.side_effect = ((display_util.CANCEL, -1), + (display_util.OK, self.path,)) - self.assertTrue(mock_display.notification.called) - for call in mock_display.notification.call_args_list: - self.assertTrue(imaginary_dir in call[0][0]) + self.auth.perform([self.achall]) - self.assertTrue(mock_display.directory_select.called) - for call in mock_display.directory_select.call_args_list: - self.assertTrue(self.achall.domain in call[0][0]) + self.assertEqual(self.config.webroot_map[self.achall.domain], self.path) + + @test_util.patch_get_utility() + 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) + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) def test_perform_missing_root(self): self.config.webroot_path = None @@ -136,27 +143,42 @@ self.assertRaises(errors.PluginError, self.auth.perform, []) os.chmod(self.path, 0o700) + @test_util.skip_on_windows('On Windows, there is no chown.') @mock.patch("certbot.plugins.webroot.os.chown") def test_failed_chown(self, mock_chown): mock_chown.side_effect = OSError(errno.EACCES, "msg") self.auth.perform([self.achall]) # exception caught and logged + + @test_util.patch_get_utility() + 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]) + self.assertEqual(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]) - path_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode) - self.assertEqual(path_permissions, 0o644) + self.assertTrue(compat.compare_file_modes(os.stat(self.validation_path).st_mode, 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) - dir_permissions = stat.S_IMODE(os.stat(full_path).st_mode) - self.assertEqual(dir_permissions, 0o755) + self.assertTrue(compat.compare_file_modes(os.stat(full_path).st_mode, 0o755)) parent_gid = os.stat(self.path).st_gid parent_uid = os.stat(self.path).st_uid @@ -179,6 +201,35 @@ self.auth.cleanup([self.achall]) self.assertFalse(os.path.exists(self.validation_path)) self.assertFalse(os.path.exists(self.root_challenge_path)) + self.assertFalse(os.path.exists(self.partial_root_challenge_path)) + + def test_perform_cleanup_existing_dirs(self): + os.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 + self.assertFalse(os.path.exists(self.validation_path)) + self.assertFalse(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" + os.mkdir(self.partial_root_challenge_path) + self.auth.prepare() + self.auth.perform([bingo_achall, self.achall]) + + self.auth.cleanup([self.achall]) + self.assertFalse(os.path.exists(bingo_validation_path)) + self.assertTrue(os.path.exists(self.root_challenge_path)) + self.auth.cleanup([bingo_achall]) + self.assertFalse(os.path.exists(self.validation_path)) + self.assertFalse(os.path.exists(self.root_challenge_path)) def test_cleanup_leftovers(self): self.auth.prepare() @@ -223,7 +274,7 @@ def test_webroot_map_action(self): args = self.parser.parse_args( - ["--webroot-map", '{{"thing.com":"{0}"}}'.format(self.path)]) + ["--webroot-map", json.dumps({'thing.com': self.path})]) self.assertEqual(args.webroot_map["thing.com"], self.path) def test_domain_before_webroot(self): diff -Nru python-certbot-0.10.2/certbot/renewal.py python-certbot-0.28.0/certbot/renewal.py --- python-certbot-0.10.2/certbot/renewal.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/renewal.py 2018-11-07 21:14:56.000000000 +0000 @@ -11,14 +11,17 @@ import OpenSSL -from certbot import cli +from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module +from certbot import cli from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot import util from certbot import hooks from certbot import storage +from certbot import updater + from certbot.plugins import disco as plugins_disco logger = logging.getLogger(__name__) @@ -30,9 +33,11 @@ STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent", "server", "account", "authenticator", "installer", "standalone_supported_challenges", "renew_hook", - "pre_hook", "post_hook"] + "pre_hook", "post_hook", "tls_sni_01_address", + "http01_address"] INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"] -BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names"] +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',))) @@ -57,8 +62,8 @@ """ try: renewal_candidate = storage.RenewableCert(full_path, config) - except (errors.CertStorageError, IOError) as exc: - logger.warning(exc) + except (errors.CertStorageError, IOError): + logger.warning("", exc_info=True) logger.warning("Renewal configuration file %s is broken. Skipping.", full_path) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None @@ -79,7 +84,7 @@ except (ValueError, errors.Error) as error: logger.warning( "An error occurred while parsing %s. The error was %s. " - "Skipping the file.", full_path, error.message) + "Skipping the file.", full_path, str(error)) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None @@ -103,13 +108,13 @@ """ if "webroot_map" in renewalparams: if not cli.set_by_cli("webroot_map"): - config.namespace.webroot_map = renewalparams["webroot_map"] + config.webroot_map = renewalparams["webroot_map"] elif "webroot_path" in renewalparams: logger.debug("Ancient renewal conf file without webroot-map, restoring webroot-path") wp = renewalparams["webroot_path"] - if isinstance(wp, str): # prior to 0.1.0, webroot_path was a string + if isinstance(wp, six.string_types): # prior to 0.1.0, webroot_path was a string wp = [wp] - config.namespace.webroot_path = wp + config.webroot_path = wp def _restore_plugin_configs(config, renewalparams): @@ -131,14 +136,15 @@ # 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 = [] # type: List[str] if renewalparams["authenticator"] == "webroot": _restore_webroot_config(config, renewalparams) - plugin_prefixes = [] else: - plugin_prefixes = [renewalparams["authenticator"]] + plugin_prefixes.append(renewalparams["authenticator"]) - if renewalparams.get("installer", None) is not None: + 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 six.iteritems(renewalparams): @@ -148,10 +154,10 @@ if config_value in ("None", "True", "False"): # bool("False") == True # pylint: disable=eval-used - setattr(config.namespace, config_item, eval(config_value)) + setattr(config, config_item, eval(config_value)) else: cast = cli.argparse_type(config_item) - setattr(config.namespace, config_item, cast(config_value)) + setattr(config, config_item, cast(config_value)) def restore_required_config_elements(config, renewalparams): @@ -172,7 +178,7 @@ for item_name, restore_func in required_items: if item_name in renewalparams and not cli.set_by_cli(item_name): value = restore_func(item_name, renewalparams[item_name]) - setattr(config.namespace, item_name, value) + setattr(config, item_name, value) def _restore_pref_challs(unused_name, value): @@ -193,7 +199,7 @@ # 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 + value = [value] if isinstance(value, six.string_types) else value return cli.parse_preferred_challenges(value) @@ -256,7 +262,7 @@ if config.renew_by_default: logger.debug("Auto-renewal forced with --force-renewal...") return True - if lineage.should_autorenew(interactive=True): + if lineage.should_autorenew(): logger.info("Cert is due for renewal, auto-renewing...") return True if config.dry_run: @@ -293,15 +299,15 @@ _avoid_invalidating_lineage(config, lineage, original_server) if not domains: domains = lineage.names() - new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains) + # The private key is the existing lineage private key if reuse_key is set. + # Otherwise, generate a fresh private key by passing None. + new_key = os.path.normpath(lineage.privkey) if config.reuse_key else 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() - new_cert = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped) - new_chain = crypto_util.dump_pyopenssl_chain(new_chain) # 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()) @@ -317,8 +323,14 @@ def _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures): - out = [] + out = [] # type: List[str] notify = out.append + disp = zope.component.getUtility(interfaces.IDisplay) + + def notify_error(err): + """Notify and log errors.""" + notify(str(err)) + logger.error(err) if config.dry_run: notify("** DRY RUN: simulating 'certbot renew' close to cert expiry") @@ -337,17 +349,17 @@ "have been renewed:") notify(report(renew_successes, "success")) elif renew_failures and not renew_successes: - notify("All renewal attempts failed. The following certs could not be " - "renewed:") - notify(report(renew_failures, "failure")) + notify_error("All renewal attempts failed. The following certs could " + "not be renewed:") + notify_error(report(renew_failures, "failure")) elif renew_failures and renew_successes: notify("The following certs were successfully renewed:") - notify(report(renew_successes, "success")) - notify("\nThe following certs could not be renewed:") - notify(report(renew_failures, "failure")) + notify(report(renew_successes, "success") + "\n") + notify_error("The following certs could not be renewed:") + notify_error(report(renew_failures, "failure")) if parse_failures: - notify("\nAdditionally, the following renewal configuration files " + notify("\nAdditionally, the following renewal configurations " "were invalid: ") notify(report(parse_failures, "parsefail")) @@ -355,9 +367,7 @@ notify("** DRY RUN: simulating 'certbot renew' close to cert expiry") notify("** (The test certificates above have not been saved.)") - if config.quiet and not (renew_failures or parse_failures): - return - print("\n".join(out)) + disp.notification("\n".join(out), wrap=False) def handle_renewal_request(config): @@ -371,8 +381,8 @@ "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. The renew verb may provide other options " + "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: @@ -388,14 +398,16 @@ disp = zope.component.getUtility(interfaces.IDisplay) disp.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.warning("Renewal configuration file %s produced an " - "unexpected error: %s. Skipping.", renewal_file, e) + logger.warning("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 @@ -407,17 +419,30 @@ # XXX: ensure that each call here replaces the previous one zope.component.provideUtility(lineage_config) renewal_candidate.ensure_deployed() + from certbot import main + plugins = plugins_disco.PluginsRegistry.find_all() if should_renew(lineage_config, renewal_candidate): - plugins = plugins_disco.PluginsRegistry.find_all() - from certbot import main - main.obtain_cert(lineage_config, plugins, renewal_candidate) + # 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) else: - renew_skipped.append(renewal_candidate.fullchain) + 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.warning("Attempting to renew cert from %s produced an " - "unexpected error: %s. Skipping.", renewal_file, e) + logger.warning("Attempting to renew cert (%s) from %s produced an " + "unexpected error: %s. Skipping.", lineagename, + renewal_file, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) renew_failures.append(renewal_candidate.fullchain) diff -Nru python-certbot-0.10.2/certbot/reporter.py python-certbot-0.28.0/certbot/reporter.py --- python-certbot-0.10.2/certbot/reporter.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/reporter.py 2018-11-07 21:14:56.000000000 +0000 @@ -3,11 +3,10 @@ import collections import logging -import os import sys import textwrap -from six.moves import queue # pylint: disable=import-error +from six.moves import queue # type: ignore # pylint: disable=import-error import zope.interface from certbot import interfaces @@ -16,11 +15,6 @@ logger = logging.getLogger(__name__) -# Store the pid of the process that first imported this module so that -# atexit_print_messages side-effects such as error reporting can be limited to -# this process and not any fork()'d children. -INITIAL_PID = os.getpid() - @zope.interface.implementer(interfaces.IReporter) class Reporter(object): @@ -60,19 +54,6 @@ self.messages.put(self._msg_type(priority, msg, on_crash)) logger.debug("Reporting to user: %s", msg) - def atexit_print_messages(self, pid=None): - """Function to be registered with atexit to print messages. - - :param int pid: Process ID - - """ - if pid is None: - pid = INITIAL_PID - # This ensures that messages are only printed from the process that - # created the Reporter. - if pid == os.getpid(): - self.print_messages() - def print_messages(self): """Prints messages to the user and clears the message queue. diff -Nru python-certbot-0.10.2/certbot/reverter.py python-certbot-0.28.0/certbot/reverter.py --- python-certbot-0.10.2/certbot/reverter.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/reverter.py 2018-11-07 21:14:56.000000000 +0000 @@ -10,6 +10,7 @@ import six import zope.component +from certbot import compat from certbot import constants from certbot import errors from certbot import interfaces @@ -65,7 +66,7 @@ self.config = config util.make_or_verify_dir( - config.backup_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), + config.backup_dir, constants.CONFIG_DIRS_MODE, compat.os_geteuid(), self.config.strict_permissions) def revert_temporary_config(self): @@ -82,8 +83,10 @@ self._recover_checkpoint(self.config.temp_checkpoint_dir) except errors.ReverterError: # We have a partial or incomplete recovery - logger.fatal("Incomplete or failed recovery for %s", - self.config.temp_checkpoint_dir) + 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=1): @@ -123,7 +126,7 @@ try: self._recover_checkpoint(cp_dir) except errors.ReverterError: - logger.fatal("Failed to load checkpoint during rollback") + logger.critical("Failed to load checkpoint during rollback") raise errors.ReverterError( "Unable to load checkpoint during rollback") rollback -= 1 @@ -181,7 +184,7 @@ if for_logging: return os.linesep.join(output) zope.component.getUtility(interfaces.IDisplay).notification( - os.linesep.join(output), force_interactive=True) + os.linesep.join(output), force_interactive=True, pause=False) def add_to_temp_checkpoint(self, save_files, save_notes): """Add files to temporary checkpoint. @@ -217,7 +220,7 @@ """ util.make_or_verify_dir( - cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), + cp_dir, constants.CONFIG_DIRS_MODE, compat.os_geteuid(), self.config.strict_permissions) op_fd, existing_filepaths = self._read_and_append( @@ -431,7 +434,7 @@ cp_dir = self.config.in_progress_dir util.make_or_verify_dir( - cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), + cp_dir, constants.CONFIG_DIRS_MODE, compat.os_geteuid(), self.config.strict_permissions) return cp_dir @@ -457,7 +460,7 @@ self._recover_checkpoint(self.config.in_progress_dir) except errors.ReverterError: # We have a partial or incomplete recovery - logger.fatal("Incomplete or failed recovery for IN_PROGRESS " + logger.critical("Incomplete or failed recovery for IN_PROGRESS " "checkpoint - %s", self.config.in_progress_dir) raise errors.ReverterError( @@ -491,10 +494,10 @@ else: logger.warning( "File: %s - Could not be found to be deleted %s - " - "LE probably shut down unexpectedly", + "Certbot probably shut down unexpectedly", os.linesep, path) except (IOError, OSError): - logger.fatal( + logger.critical( "Unable to remove filepaths contained within %s", file_list) raise errors.ReverterError( "Unable to remove filepaths contained within " @@ -573,7 +576,7 @@ timestamp = self._checkpoint_timestamp() final_dir = os.path.join(self.config.backup_dir, timestamp) try: - os.rename(self.config.in_progress_dir, final_dir) + compat.os_rename(self.config.in_progress_dir, final_dir) return except OSError: logger.warning("Extreme, unexpected race condition, retrying (%s)", timestamp) diff -Nru python-certbot-0.10.2/certbot/ssl-dhparams.pem python-certbot-0.28.0/certbot/ssl-dhparams.pem --- python-certbot-0.10.2/certbot/ssl-dhparams.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/ssl-dhparams.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== +-----END DH PARAMETERS----- diff -Nru python-certbot-0.10.2/certbot/storage.py python-certbot-0.28.0/certbot/storage.py --- python-certbot-0.10.2/certbot/storage.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/storage.py 2018-11-07 21:14:56.000000000 +0000 @@ -4,6 +4,7 @@ import logging import os import re +import stat import configobj import parsedatetime @@ -13,12 +14,16 @@ import certbot from certbot import cli +from certbot import compat from certbot import constants from certbot import crypto_util from certbot import errors from certbot import error_handler from certbot import util +from certbot.plugins import common as plugins_common +from certbot.plugins import disco as plugins_disco + logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") @@ -27,7 +32,14 @@ def renewal_conf_files(config): - """Return /path/to/*.conf in the renewal conf directory""" + """Build a list of all renewal configuration files. + + :param certbot.interfaces.IConfig config: Configuration object + + :returns: list of renewal configuration files + :rtype: `list` of `str` + + """ return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) def renewal_file_for_certname(config, certname): @@ -38,6 +50,21 @@ "{1}).".format(certname, path)) return path + +def cert_path_for_cert_name(config, cert_name): + """ 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) + fullchain_path = configobj.ConfigObj(cert_name_implied_conf)["fullchain"] + with open(fullchain_path) as f: + cert_path = (fullchain_path, f.read()) + return cert_path + + def config_with_defaults(config=None): """Merge supplied config, if provided, on top of builtin defaults.""" defaults_copy = configobj.ConfigObj(constants.RENEWER_DEFAULTS) @@ -107,10 +134,20 @@ # TODO: add human-readable comments explaining other available # parameters logger.debug("Writing new config %s.", n_filename) + + # Ensure that the file exists + open(n_filename, 'a').close() + + # 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) + os.chmod(n_filename, current_permissions) + with open(n_filename, "wb") as f: config.write(outfile=f) return config + def rename_renewal_config(prev_name, new_name, cli_config): """Renames cli_config.certname's config to cli_config.new_certname. @@ -152,7 +189,7 @@ # Save only the config items that are relevant to renewal values = relevant_values(vars(cli_config.namespace)) write_renewal_config(config_filename, temp_filename, archive_dir, target, values) - os.rename(temp_filename, config_filename) + compat.os_rename(temp_filename, config_filename) return configobj.ConfigObj(config_filename) @@ -165,12 +202,39 @@ :returns: Absolute path to the target of link :rtype: str + :raises .CertStorageError: If link does not exists. + """ - target = os.readlink(link) + try: + target = os.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, is_base_dir=False): + 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(option): """ @@ -179,13 +243,12 @@ :rtype: bool """ - # The list() here produces a list of the plugin names as strings. from certbot import renewal - from certbot.plugins import disco as plugins_disco - plugins = list(plugins_disco.PluginsRegistry.find_all()) + plugins = plugins_disco.PluginsRegistry.find_all() + namespaces = [plugins_common.dest_namespace(plugin) for plugin in plugins] return (option in renewal.CONFIG_ITEMS or - any(option.startswith(x + "_") for x in plugins)) + any(option.startswith(namespace) for namespace in namespaces)) def relevant_values(all_values): @@ -197,10 +260,15 @@ :rtype dict: """ - return dict( + rv = dict( (option, value) for option, value in six.iteritems(all_values) if _relevant(option) and cli.option_was_set(option, value)) + # 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"] + return rv def lineagename_for_filename(config_filename): """Returns the lineagename for a configuration filename. @@ -219,7 +287,7 @@ """Path to a directory from a file""" return os.path.relpath(archive_dir, os.path.dirname(from_file)) -def _full_archive_path(config_obj, cli_config, lineagename): +def full_archive_path(config_obj, cli_config, lineagename): """Returns the full archive path for a lineagename Uses cli_config to determine archive path if not available from config_obj. @@ -244,7 +312,7 @@ """ renewal_filename = renewal_file_for_certname(config, certname) # file exists - full_default_archive_dir = _full_archive_path(None, config, certname) + 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) @@ -296,7 +364,7 @@ # archive directory try: - archive_path = _full_archive_path(renewal_config, config, certname) + archive_path = full_archive_path(renewal_config, config, certname) shutil.rmtree(archive_path) logger.debug("Removed %s", archive_path) except OSError: @@ -375,7 +443,7 @@ conf_version = self.configuration.get("version") if (conf_version is not None and util.get_strict_version(conf_version) > CURRENT_VERSION): - logger.warning( + 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__) @@ -392,6 +460,26 @@ self._check_symlinks() @property + def key_path(self): + """Duck type for self.privkey""" + return self.privkey + + @property + def cert_path(self): + """Duck type for self.cert""" + return self.cert + + @property + def chain_path(self): + """Duck type for self.chain""" + return self.chain + + @property + def fullchain_path(self): + """Duck type for self.fullchain""" + return self.fullchain + + @property def target_expiry(self): """The current target certificate's expiration datetime @@ -403,7 +491,7 @@ @property def archive_dir(self): """Returns the default or specified archive directory""" - return _full_archive_path(self.configuration, + return full_archive_path(self.configuration, self.cli_config, self.lineagename) def relative_archive_dir(self, from_file): @@ -716,7 +804,7 @@ :returns: ``True`` if there is a complete version of this lineage with a larger version number than the current - version, and ``False`` otherwis + version, and ``False`` otherwise :rtype: bool """ @@ -858,10 +946,10 @@ :rtype: bool """ - return ("autorenew" not in self.configuration or - self.configuration.as_bool("autorenew")) + return ("autorenew" not in self.configuration["renewalparams"] or + self.configuration["renewalparams"].as_bool("autorenew")) - def should_autorenew(self, interactive=False): + def should_autorenew(self): """Should we now try to autorenew the most recent cert version? This is a policy question and does not only depend on whether @@ -872,16 +960,12 @@ Note that this examines the numerically most recent cert version, not the currently deployed version. - :param bool interactive: set to True to examine the question - regardless of whether the renewal configuration allows - automated renewal (for interactive use). Default False. - :returns: whether an attempt should now be made to autorenew the most current cert version in this lineage :rtype: bool """ - if interactive or self.autorenewal_is_enabled(): + if self.autorenewal_is_enabled(): # Consider whether to attempt to autorenew this cert now # Renewals on the basis of revocation @@ -940,12 +1024,15 @@ 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) + archive = full_archive_path(None, cli_config, lineagename) live_dir = _full_live_path(cli_config, lineagename) if os.path.exists(archive): raise errors.CertStorageError( @@ -982,18 +1069,7 @@ # Write a README file to the live directory readme_path = os.path.join(live_dir, README) - 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" - "`privkey.pem` : the private key for your certificate.\n" - "`fullchain.pem`: the certificate file used in most server software.\n" - "`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.\n" - "`cert.pem` : will break many server configurations, and " - "should not be used\n" - " without reading further documentation (see link below).\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") + _write_live_readme_to(readme_path) # Document what we've done in a new renewal config file config_file.close() @@ -1018,10 +1094,10 @@ 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 str new_cert: the new certificate, in PEM format - :param str new_privkey: the new private key, in PEM format, + :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 str new_chain: the new chain, in PEM format + :param bytes new_chain: the new chain, in PEM format :param .NamespaceConfig cli_config: parsed command line arguments @@ -1057,18 +1133,18 @@ logger.debug("Writing symlink to old private key, %s.", old_privkey) os.symlink(old_privkey, target["privkey"]) else: - with open(target["privkey"], "w") as f: + with open(target["privkey"], "wb") as f: logger.debug("Writing new private key to %s.", target["privkey"]) f.write(new_privkey) # Save everything else - with open(target["cert"], "w") as f: + with open(target["cert"], "wb") as f: logger.debug("Writing certificate to %s.", target["cert"]) f.write(new_cert) - with open(target["chain"], "w") as f: + with open(target["chain"], "wb") as f: logger.debug("Writing chain to %s.", target["chain"]) f.write(new_chain) - with open(target["fullchain"], "w") as f: + with open(target["fullchain"], "wb") as f: logger.debug("Writing full chain to %s.", target["fullchain"]) f.write(new_cert + new_chain) diff -Nru python-certbot-0.10.2/certbot/tests/account_test.py python-certbot-0.28.0/certbot/tests/account_test.py --- python-certbot-0.10.2/certbot/tests/account_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/account_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,23 +1,23 @@ """Tests for certbot.account.""" import datetime +import json import os import shutil import stat -import tempfile import unittest +import josepy as jose import mock import pytz -from acme import jose from acme import messages from certbot import errors -from certbot.tests import util +import certbot.tests.util as test_util -KEY = jose.JWKRSA.load(util.load_vector("rsa512_key_2.pem")) +KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class AccountTest(unittest.TestCase): @@ -31,6 +31,7 @@ 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.account.socket") as mock_socket: mock_socket.getfqdn.return_value = "test.certbot.org" @@ -45,30 +46,22 @@ def test_id(self): self.assertEqual( - self.acc.id, "bca5889f66457d5b62fbba7b25f9ab6f") + self.acc.id, "7adac10320f585ddf118429c0c4af2cd") def test_slug(self): self.assertEqual( - self.acc.slug, "test.certbot.org@2015-07-04T14:04:10Z (bca5)") + self.acc.slug, "test.certbot.org@2015-07-04T14:04:10Z (7ada)") def test_repr(self): - self.assertEqual( - repr(self.acc), - "") - + self.assertTrue(repr(self.acc).startswith( + " 0) def test_view_config_changes_bad_backups_dir(self): - # There shouldn't be any "in progess directories when this is called + # There shouldn't be any "in progress directories when this is called # It must just be clean checkpoints os.makedirs(os.path.join(self.config.backup_dir, "in_progress")) @@ -439,21 +449,6 @@ return config3 -def setup_work_direc(): - """Setup directories. - - :returns: Mocked :class:`certbot.interfaces.IConfig` - - """ - work_dir = tempfile.mkdtemp("work") - backup_dir = os.path.join(work_dir, "backup") - - return mock.MagicMock( - work_dir=work_dir, backup_dir=backup_dir, - temp_checkpoint_dir=os.path.join(work_dir, "temp"), - in_progress_dir=os.path.join(backup_dir, "in_progress_dir")) - - def setup_test_files(): """Setup sample configuration files.""" dir1 = tempfile.mkdtemp("dir1") diff -Nru python-certbot-0.10.2/certbot/tests/storage_test.py python-certbot-0.28.0/certbot/tests/storage_test.py --- python-certbot-0.10.2/certbot/tests/storage_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/storage_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -3,7 +3,7 @@ import datetime import os import shutil -import tempfile +import stat import unittest import configobj @@ -13,14 +13,14 @@ import certbot from certbot import cli -from certbot import configuration from certbot import errors from certbot.storage import ALL_FOUR -from certbot.tests import util +import certbot.tests.util as test_util -CERT = util.load_cert('cert.pem') +CERT = test_util.load_cert('cert_512.pem') + def unlink_all(rc_object): @@ -36,52 +36,44 @@ f.write(kind) -class BaseRenewableCertTest(unittest.TestCase): +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. """ - _multiprocess_can_split_ = True def setUp(self): from certbot import storage - self.tempdir = tempfile.mkdtemp() - self.cli_config = configuration.NamespaceConfig( - namespace=mock.MagicMock( - config_dir=self.tempdir, - work_dir=self.tempdir, - logs_dir=self.tempdir, - ) - ) + super(BaseRenewableCertTest, self).setUp() # TODO: maybe provide NamespaceConfig.make_dirs? # TODO: main() should create those dirs, c.f. #902 - os.makedirs(os.path.join(self.tempdir, "live", "example.org")) - archive_path = os.path.join(self.tempdir, "archive", "example.org") + os.makedirs(os.path.join(self.config.config_dir, "live", "example.org")) + archive_path = os.path.join(self.config.config_dir, "archive", "example.org") os.makedirs(archive_path) - os.makedirs(os.path.join(self.tempdir, "renewal")) + os.makedirs(os.path.join(self.config.config_dir, "renewal")) - config = configobj.ConfigObj() + config_file = configobj.ConfigObj() for kind in ALL_FOUR: - kind_path = os.path.join(self.tempdir, "live", "example.org", + kind_path = os.path.join(self.config.config_dir, "live", "example.org", kind + ".pem") - config[kind] = kind_path - with open(os.path.join(self.tempdir, "live", "example.org", + config_file[kind] = kind_path + with open(os.path.join(self.config.config_dir, "live", "example.org", "README"), 'a'): pass - config["archive"] = archive_path - config.filename = os.path.join(self.tempdir, "renewal", + config_file["archive"] = archive_path + config_file.filename = os.path.join(self.config.config_dir, "renewal", "example.org.conf") - config.write() - self.config = config + 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. - junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w") + junk = open(os.path.join(self.config.config_dir, "renewal", "IGNORE.THIS"), "w") junk.write("This file should be ignored!") junk.close() @@ -89,10 +81,7 @@ with mock.patch("certbot.storage.RenewableCert._check_symlinks") as check: check.return_value = True - self.test_rc = storage.RenewableCert(config.filename, self.cli_config) - - def tearDown(self): - shutil.rmtree(self.tempdir) + 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) @@ -119,7 +108,7 @@ for kind in ALL_FOUR: self.assertEqual( getattr(self.test_rc, kind), os.path.join( - self.tempdir, "live", "example.org", kind + ".pem")) + self.config.config_dir, "live", "example.org", kind + ".pem")) def test_renewal_bad_config(self): """Test that the RenewableCert constructor will complain if @@ -127,14 +116,14 @@ """ from certbot import storage - broken = os.path.join(self.tempdir, "broken.conf") + broken = os.path.join(self.config.config_dir, "broken.conf") with open(broken, "w") as f: f.write("[No closing bracket for you!") self.assertRaises(errors.CertStorageError, storage.RenewableCert, - broken, self.cli_config) + broken, self.config) os.unlink(broken) self.assertRaises(errors.CertStorageError, storage.RenewableCert, - "fun", self.cli_config) + "fun", self.config) def test_renewal_incomplete_config(self): """Test that the RenewableCert constructor will complain if @@ -145,32 +134,32 @@ # Here the required privkey is missing. config["chain"] = "imaginary_chain.pem" config["fullchain"] = "imaginary_fullchain.pem" - config.filename = os.path.join(self.tempdir, "imaginary_config.conf") + config.filename = os.path.join(self.config.config_dir, "imaginary_config.conf") config.write() self.assertRaises(errors.CertStorageError, storage.RenewableCert, - config.filename, self.cli_config) + config.filename, self.config) def test_no_renewal_version(self): from certbot import storage self._write_out_ex_kinds() - self.assertTrue("version" not in self.config) + self.assertTrue("version" not in self.config_file) with mock.patch("certbot.storage.logger") as mock_logger: - storage.RenewableCert(self.config.filename, self.cli_config) + storage.RenewableCert(self.config_file.filename, self.config) self.assertFalse(mock_logger.warning.called) def test_renewal_newer_version(self): from certbot import storage self._write_out_ex_kinds() - self.config["version"] = "99.99.99" - self.config.write() + self.config_file["version"] = "99.99.99" + self.config_file.write() with mock.patch("certbot.storage.logger") as mock_logger: - storage.RenewableCert(self.config.filename, self.cli_config) - self.assertTrue(mock_logger.warning.called) - self.assertTrue("version" in mock_logger.warning.call_args[0][0]) + storage.RenewableCert(self.config_file.filename, self.config) + self.assertTrue(mock_logger.info.called) + self.assertTrue("version" in mock_logger.info.call_args[0][0]) def test_consistent(self): # pylint: disable=too-many-statements,protected-access @@ -193,7 +182,7 @@ 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.tempdir, kind + "17.pem"), + os.symlink(os.path.join(self.config.config_dir, kind + "17.pem"), getattr(self.test_rc, kind)) self.assertFalse(self.test_rc._consistent()) unlink_all(self.test_rc) @@ -218,17 +207,17 @@ # Relative path logic self._write_out_kind("cert", 17) self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), - os.path.join(self.tempdir, "archive", + 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.tempdir, "archive", "example.org", + 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") self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), - os.path.join(self.tempdir, "archive", + os.path.join(self.config.config_dir, "archive", "example.org", "cert17.pem"))) @@ -371,18 +360,21 @@ def test_names(self): # Trying the current version - self._write_out_kind("cert", 12, util.load_vector("cert-san.pem")) + self._write_out_kind("cert", 12, test_util.load_vector("cert-san_512.pem")) + self.assertEqual(self.test_rc.names(), ["example.com", "www.example.com"]) # Trying a non-current version - self._write_out_kind("cert", 15, util.load_vector("cert.pem")) + self._write_out_kind("cert", 15, test_util.load_vector("cert_512.pem")) + self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) # Testing common name is listed first self._write_out_kind( - "cert", 12, util.load_vector("cert-5sans.pem")) + "cert", 12, test_util.load_vector("cert-5sans_512.pem")) + self.assertEqual( self.test_rc.names(12), ["example.com"] + ["{0}.example.com".format(c) for c in "abcd"]) @@ -391,11 +383,13 @@ os.unlink(self.test_rc.cert) self.assertRaises(errors.CertStorageError, self.test_rc.names) + @mock.patch("certbot.storage.cli") @mock.patch("certbot.storage.datetime") - def test_time_interval_judgments(self, mock_datetime): + def test_time_interval_judgments(self, mock_datetime, mock_cli): """Test should_autodeploy() and should_autorenew() on the basis of expiry time windows.""" - test_cert = util.load_vector("cert.pem") + test_cert = test_util.load_vector("cert_512.pem") + self._write_out_ex_kinds() self.test_rc.update_all_links_to(12) @@ -406,6 +400,8 @@ f.write(test_cert) mock_datetime.timedelta = datetime.timedelta + mock_cli.set_by_cli.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) @@ -458,22 +454,25 @@ self.assertFalse(self.test_rc.should_autodeploy()) def test_autorenewal_is_enabled(self): + self.test_rc.configuration["renewalparams"] = {} self.assertTrue(self.test_rc.autorenewal_is_enabled()) - self.test_rc.configuration["autorenew"] = "1" + self.test_rc.configuration["renewalparams"]["autorenew"] = "True" self.assertTrue(self.test_rc.autorenewal_is_enabled()) - self.test_rc.configuration["autorenew"] = "0" + self.test_rc.configuration["renewalparams"]["autorenew"] = "False" self.assertFalse(self.test_rc.autorenewal_is_enabled()) + @mock.patch("certbot.storage.cli") @mock.patch("certbot.storage.RenewableCert.ocsp_revoked") - def test_should_autorenew(self, mock_ocsp): + def test_should_autorenew(self, mock_ocsp, mock_cli): """Test should_autorenew on the basis of reasons other than expiry time window.""" # pylint: disable=too-many-statements + mock_cli.set_by_cli.return_value = False # Autorenewal turned off - self.test_rc.configuration["autorenew"] = "0" + self.test_rc.configuration["renewalparams"] = {"autorenew": "False"} self.assertFalse(self.test_rc.should_autorenew()) - self.test_rc.configuration["autorenew"] = "1" + 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 @@ -481,6 +480,7 @@ self.assertTrue(self.test_rc.should_autorenew()) mock_ocsp.return_value = False + @test_util.broken_on_windows @mock.patch("certbot.storage.relevant_values") def test_save_successor(self, mock_rv): # Mock relevant_values() to claim that all values are relevant here @@ -492,8 +492,8 @@ self._write_out_kind(kind, ver) self.test_rc.update_all_links_to(3) self.assertEqual( - 6, self.test_rc.save_successor(3, "new cert", None, - "new chain", self.cli_config)) + 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: self.assertEqual(f.read(), "new cert") with open(self.test_rc.version("chain", 6)) as f: @@ -505,11 +505,11 @@ self.assertTrue(os.path.islink(self.test_rc.version("privkey", 6))) # Let's try two more updates self.assertEqual( - 7, self.test_rc.save_successor(6, "again", None, - "newer chain", self.cli_config)) + 7, self.test_rc.save_successor(6, b'again', None, + b'newer chain', self.config)) self.assertEqual( - 8, self.test_rc.save_successor(7, "hello", None, - "other chain", self.cli_config)) + 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): @@ -523,36 +523,45 @@ # Test updating from latest version rather than old version self.test_rc.update_all_links_to(8) self.assertEqual( - 9, self.test_rc.save_successor(8, "last", None, - "attempt", self.cli_config)) + 9, self.test_rc.save_successor(8, b'last', None, + b'attempt', self.config)) for kind in ALL_FOUR: self.assertEqual(self.test_rc.available_versions(kind), list(six.moves.range(1, 10))) self.assertEqual(self.test_rc.current_version(kind), 8) with open(self.test_rc.version("fullchain", 9)) as f: self.assertEqual(f.read(), "last" + "attempt") - temp_config_file = os.path.join(self.cli_config.renewal_configs_dir, + 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. self.assertEqual( - 10, self.test_rc.save_successor(9, "with", "a", - "key", self.cli_config)) + 10, self.test_rc.save_successor(9, b'with', b'a', + b'key', self.config)) self.assertTrue(os.path.exists(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file)) def _test_relevant_values_common(self, values): - option = "rsa_key_size" - mock_parser = mock.Mock(args=["--standalone"], verb="certonly", - defaults={option: cli.flag_default(option)}) + defaults = dict((option, cli.flag_default(option)) + for option in ("authenticator", "installer", + "rsa_key_size", "server",)) + mock_parser = mock.Mock(args=[], verb="plugins", + defaults=defaults) + + # make a copy to ensure values isn't modified + values = values.copy() + values.setdefault("server", defaults["server"]) + expected_server = values["server"] from certbot.storage import relevant_values with mock.patch("certbot.cli.helpful_parser", mock_parser): - # make a copy to ensure values isn't modified - return relevant_values(values.copy()) + rv = relevant_values(values) + self.assertIn("server", rv) + self.assertEqual(rv.pop("server"), expected_server) + return rv def test_relevant_values(self): """Test that relevant_values() can reject an irrelevant value.""" @@ -581,6 +590,26 @@ self.assertEqual( self._test_relevant_values_common(values), values) + def test_relevant_values_plugins_none(self): + self.assertEqual( + self._test_relevant_values_common( + {"authenticator": None, "installer": None}), {}) + + @mock.patch("certbot.cli.set_by_cli") + @mock.patch("certbot.plugins.disco.PluginsRegistry.find_all") + def test_relevant_values_namespace(self, mock_find_all, mock_set_by_cli): + mock_set_by_cli.return_value = True + mock_find_all.return_value = ["certbot-foo:bar"] + values = {"certbot_foo:bar_baz": 42} + self.assertEqual( + self._test_relevant_values_common(values), values) + + def test_relevant_values_server(self): + self.assertEqual( + # _test_relevant_values_common handles testing the server + # value and removes it + self._test_relevant_values_common({"server": "example.org"}), {}) + @mock.patch("certbot.storage.relevant_values") def test_new_lineage(self, mock_rv): """Test for new_lineage() class method.""" @@ -590,38 +619,40 @@ from certbot import storage result = storage.RenewableCert.new_lineage( - "the-lineage.com", b"cert", b"privkey", b"chain", self.cli_config) + "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 self.assertTrue(result._consistent()) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) + self.config.renewal_configs_dir, "the-lineage.com.conf"))) + self.assertTrue(os.path.exists(os.path.join( + self.config.live_dir, "README"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "the-lineage.com", "README"))) + self.config.live_dir, "the-lineage.com", "README"))) with open(result.fullchain, "rb") as f: self.assertEqual(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.cli_config) + "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf"))) + self.config.renewal_configs_dir, "the-lineage.com-0001.conf"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "the-lineage.com-0001", "README"))) + self.config.live_dir, "the-lineage.com-0001", "README"))) # Now trigger the detection of already existing files os.mkdir(os.path.join( - self.cli_config.live_dir, "the-lineage.com-0002")) + self.config.live_dir, "the-lineage.com-0002")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "the-lineage.com", - b"cert3", b"privkey3", b"chain3", self.cli_config) - os.mkdir(os.path.join(self.cli_config.default_archive_dir, "other-example.com")) + b"cert3", b"privkey3", b"chain3", self.config) + os.mkdir(os.path.join(self.config.default_archive_dir, "other-example.com")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "other-example.com", b"cert4", - b"privkey4", b"chain4", self.cli_config) + b"privkey4", b"chain4", self.config) # Make sure it can accept renewal parameters result = storage.RenewableCert.new_lineage( - "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config) + "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config) # TODO: Conceivably we could test that the renewal parameters actually # got saved @@ -633,19 +664,19 @@ mock_rv.side_effect = lambda x: x from certbot import storage - shutil.rmtree(self.cli_config.renewal_configs_dir) - shutil.rmtree(self.cli_config.default_archive_dir) - shutil.rmtree(self.cli_config.live_dir) + 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.cli_config) + "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config) self.assertTrue(os.path.exists( os.path.join( - self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) + self.config.renewal_configs_dir, "the-lineage.com.conf"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "the-lineage.com", "privkey.pem"))) + self.config.live_dir, "the-lineage.com", "privkey.pem"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.default_archive_dir, "the-lineage.com", "privkey1.pem"))) + self.config.default_archive_dir, "the-lineage.com", "privkey1.pem"))) @mock.patch("certbot.storage.util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): @@ -653,7 +684,7 @@ mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "example.com", - "cert", "privkey", "chain", self.cli_config) + "cert", "privkey", "chain", self.config) def test_bad_kind(self): self.assertRaises( @@ -724,7 +755,7 @@ self.test_rc.configuration["renewalparams"] = {} rp = self.test_rc.configuration["renewalparams"] self.assertEqual(self.test_rc.is_test_cert, False) - rp["server"] = "https://acme-staging.api.letsencrypt.org/directory" + rp["server"] = "https://acme-staging-v02.api.letsencrypt.org/directory" self.assertEqual(self.test_rc.is_test_cert, True) rp["server"] = "https://staging.someotherca.com/directory" self.assertEqual(self.test_rc.is_test_cert, True) @@ -737,28 +768,31 @@ from certbot import storage self.assertRaises(errors.CertStorageError, storage.RenewableCert, - self.config.filename, self.cli_config) - os.symlink("missing", self.config[ALL_FOUR[0]]) + self.config_file.filename, self.config) + os.symlink("missing", self.config_file[ALL_FOUR[0]]) self.assertRaises(errors.CertStorageError, storage.RenewableCert, - self.config.filename, self.cli_config) + 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.tempdir, "sample-file") - temp2 = os.path.join(self.tempdir, "sample-file.new") + 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") + os.chmod(temp, 0o640) target = {} for x in ALL_FOUR: target[x] = "somewhere" archive_dir = "the_archive" relevant_data = {"useful": "new_value"} + from certbot 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 @@ -769,111 +803,134 @@ self.assertTrue("useless" not in content) # check version was stored self.assertTrue("version = {0}".format(certbot.__version__) in content) + # ensure permissions are copied + self.assertEqual(stat.S_IMODE(os.lstat(temp).st_mode), + stat.S_IMODE(os.lstat(temp2).st_mode)) def test_update_symlinks(self): from certbot import storage - archive_dir_path = os.path.join(self.tempdir, "archive", "example.org") + archive_dir_path = os.path.join(self.config.config_dir, "archive", "example.org") for kind in ALL_FOUR: - live_path = self.config[kind] + 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.tempdir, basename), live_path) + os.symlink(os.path.join(self.config.config_dir, basename), live_path) self.assertRaises(errors.CertStorageError, - storage.RenewableCert, self.config.filename, - self.cli_config) - storage.RenewableCert(self.config.filename, self.cli_config, + storage.RenewableCert, self.config_file.filename, + self.config) + storage.RenewableCert(self.config_file.filename, self.config, update_symlinks=True) class DeleteFilesTest(BaseRenewableCertTest): """Tests for certbot.storage.delete_files""" def setUp(self): super(DeleteFilesTest, self).setUp() + for kind in ALL_FOUR: - kind_path = os.path.join(self.tempdir, "live", "example.org", + kind_path = os.path.join(self.config.config_dir, "live", "example.org", kind + ".pem") with open(kind_path, 'a'): pass - self.config.write() + self.config_file.write() self.assertTrue(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "example.org.conf"))) + self.config.renewal_configs_dir, "example.org.conf"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertTrue(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def _call(self): from certbot import storage with mock.patch("certbot.storage.logger"): - storage.delete_files(self.cli_config, "example.org") + storage.delete_files(self.config, "example.org") def test_delete_all_files(self): self._call() self.assertFalse(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "example.org.conf"))) + self.config.renewal_configs_dir, "example.org.conf"))) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def test_bad_renewal_config(self): - with open(self.config.filename, 'a') as config_file: + with open(self.config_file.filename, 'a') as config_file: config_file.write("asdfasfasdfasdf") self.assertRaises(errors.CertStorageError, self._call) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.renewal_configs_dir, "example.org.conf"))) + self.config.renewal_configs_dir, "example.org.conf"))) def test_no_renewal_config(self): - os.remove(self.config.filename) + os.remove(self.config_file.filename) self.assertRaises(errors.CertStorageError, self._call) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) - self.assertFalse(os.path.exists(self.config.filename)) + self.config.live_dir, "example.org"))) + self.assertFalse(os.path.exists(self.config_file.filename)) def test_no_cert_file(self): os.remove(os.path.join( - self.cli_config.live_dir, "example.org", "cert.pem")) + self.config.live_dir, "example.org", "cert.pem")) self._call() - self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(self.config_file.filename)) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def test_no_readme_file(self): os.remove(os.path.join( - self.cli_config.live_dir, "example.org", "README")) + self.config.live_dir, "example.org", "README")) self._call() - self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(self.config_file.filename)) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def test_livedir_not_empty(self): with open(os.path.join( - self.cli_config.live_dir, "example.org", "other_file"), 'a'): + self.config.live_dir, "example.org", "other_file"), 'a'): pass self._call() - self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(self.config_file.filename)) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(os.path.join( - self.tempdir, "archive", "example.org"))) + self.config.config_dir, "archive", "example.org"))) def test_no_archive(self): - archive_dir = os.path.join(self.tempdir, "archive", "example.org") + archive_dir = os.path.join(self.config.config_dir, "archive", "example.org") os.rmdir(archive_dir) self._call() - self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(self.config_file.filename)) self.assertFalse(os.path.exists(os.path.join( - self.cli_config.live_dir, "example.org"))) + self.config.live_dir, "example.org"))) self.assertFalse(os.path.exists(archive_dir)) +class CertPathForCertNameTest(BaseRenewableCertTest): + """Test for certbot.storage.cert_path_for_cert_name""" + def setUp(self): + super(CertPathForCertNameTest, self).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.storage import cert_path_for_cert_name + return cert_path_for_cert_name(cli_config, certname) + + def test_simple_cert_name(self): + self.assertEqual(self._call(self.config, 'example.org'), (self.fullchain, 'fullchain')) + + def test_no_such_cert_name(self): + self.assertRaises(errors.CertStorageError, self._call, self.config, 'fake-example.org') if __name__ == "__main__": unittest.main() # pragma: no cover diff -Nru python-certbot-0.10.2/certbot/tests/testdata/README python-certbot-0.28.0/certbot/tests/testdata/README --- python-certbot-0.10.2/certbot/tests/testdata/README 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/README 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,11 @@ +The 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 + +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] \ No newline at end of file diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert-5sans.pem python-certbot-0.28.0/certbot/tests/testdata/cert-5sans.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert-5sans.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert-5sans.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ ------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----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert-5sans_512.pem python-certbot-0.28.0/certbot/tests/testdata/cert-5sans_512.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert-5sans_512.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert-5sans_512.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,16 @@ +-----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----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert-nosans_nistp256.pem python-certbot-0.28.0/certbot/tests/testdata/cert-nosans_nistp256.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert-nosans_nistp256.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert-nosans_nistp256.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBoDCCAUYCCQDCnzfUZ7TQdDAKBggqhkjOPQQDAjBYMQswCQYDVQQGEwJVUzER +MA8GA1UECAwITWljaGlnYW4xEjAQBgNVBAcMCUFubiBBcmJvcjEMMAoGA1UECgwD +RUZGMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xODA1MTUxNzIyMzlaFw0xODA2 +MTQxNzIyMzlaMFgxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjESMBAG +A1UEBwwJQW5uIEFyYm9yMQwwCgYDVQQKDANFRkYxFDASBgNVBAMMC2V4YW1wbGUu +Y29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPPl0JauSZukvAUWv4l5VNLAY +QXhuPXYQBf4dVET3s0E5q9ZCbSe+pNUbko9F+TFkuc7XVjQPsfkDbh0I9nD0tzAK +BggqhkjOPQQDAgNIADBFAiEAv8S2GXmWJqZ+j3DBfm72E1YK+HkOf+TOUHsbVR+O +Z1oCIFWNt1SPdIgRp4QAyzVk2pcTF8jDNajEMLWETDtxgRvM +-----END CERTIFICATE----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert-san.pem python-certbot-0.28.0/certbot/tests/testdata/cert-san.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert-san.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert-san.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ ------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----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert-san_512.pem python-certbot-0.28.0/certbot/tests/testdata/cert-san_512.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert-san_512.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert-san_512.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,14 @@ +-----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----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert.b64jose python-certbot-0.28.0/certbot/tests/testdata/cert.b64jose --- python-certbot-0.10.2/certbot/tests/testdata/cert.b64jose 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert.b64jose 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMndfk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o \ No newline at end of file Binary files /srv/release.debian.org/tmp/U7pNdq3mJD/python-certbot-0.10.2/certbot/tests/testdata/cert.der and /srv/release.debian.org/tmp/5tQRbMTy6D/python-certbot-0.28.0/certbot/tests/testdata/cert.der differ diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert.pem python-certbot-0.28.0/certbot/tests/testdata/cert.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx -ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM -IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 -YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG -A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix -KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS -BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR -7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c -+pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll -vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn -B/o= ------END CERTIFICATE----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert_2048.pem python-certbot-0.28.0/certbot/tests/testdata/cert_2048.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert_2048.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert_2048.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,20 @@ +-----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----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert_512.pem python-certbot-0.28.0/certbot/tests/testdata/cert_512.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert_512.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert_512.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx +ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM +IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 +YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG +A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix +KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS +BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR +7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c ++pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll +vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn +B/o= +-----END CERTIFICATE----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert_512_bad.pem python-certbot-0.28.0/certbot/tests/testdata/cert_512_bad.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert_512_bad.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert_512_bad.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICYzCCAg2gAwIBAgIJAPvqv4TcAtuFMA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD +VQQGEwJDQTEQMA4GA1UECAwHT250YXJpbzEQMA4GA1UEBwwHVG9yb250bzEMMAoG +A1UECgwDRUZGMRYwFAYDVQQLDA1UZWNoIFByb2plY3RzMQ4wDAYDVQQDDAVZb21u +YTEjMCEGCSqGSIb3DQEJARYUeW9tbmEubmFzc2VyQGVmZi5vcmcwHhcNMTcwMzI0 +MjIzMjUxWhcNNDgwMTI0MjIzMjUxWjCBjDELMAkGA1UEBhMCQ0ExEDAOBgNVBAgM +B09udGFyaW8xEDAOBgNVBAcMB1Rvcm9udG8xDDAKBgNVBAoMA0VGRjEWMBQGA1UE +CwwNVGVjaCBQcm9qZWN0czEOMAwGA1UEAwwFWW9tbmExIzAhBgkqhkiG9w0BCQEW +FHlvbW5hLm5hc3NlckBlZmYub3JnMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1 +c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvO +jm0c+pVE6K+EdE/twuUCAwEAAaNQME4wHQYDVR0OBBYEFCbPyO1L15Sy5CUDWCSP +BMB01ZeKMB8GA1UdIwQYMBaAFCbPyO1L15Sy5CUDWCSPBMB01ZeKMAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQELBQADQQAeWDdcrJOolFHr3m8TrlDJ/Ca4SfJya2jb +K1wahbX83sC42834HbDOQASGBhoLYDhC1cMPbKDDjMbR9rjYuf7T +-----END CERTIFICATE----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/cert_fullchain_2048.pem python-certbot-0.28.0/certbot/tests/testdata/cert_fullchain_2048.pem --- python-certbot-0.10.2/certbot/tests/testdata/cert_fullchain_2048.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/cert_fullchain_2048.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,40 @@ +-----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----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-6sans.pem python-certbot-0.28.0/certbot/tests/testdata/csr-6sans.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr-6sans.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-6sans.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw -EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy -c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG -9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0 -9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG -9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL -ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t -ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd -k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv -IvzVBz/nD11drfz/RNuX ------END CERTIFICATE REQUEST----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-6sans_512.conf python-certbot-0.28.0/certbot/tests/testdata/csr-6sans_512.conf --- python-certbot-0.10.2/certbot/tests/testdata/csr-6sans_512.conf 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-6sans_512.conf 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,29 @@ +[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 \ No newline at end of file diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-6sans_512.pem python-certbot-0.28.0/certbot/tests/testdata/csr-6sans_512.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr-6sans_512.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-6sans_512.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw +EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy +c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG +9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f +p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoIGGMIGDBgkqhkiG +9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL +ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t +ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQA+ +sU6T30n3SsdnHlj0Va8eECOWK7Lf8nUfxxgjPMQ7BoU8gbAnGfDmOlwDronTRqf1 +Me+nlYJU4TX1OiX10DYu +-----END CERTIFICATE REQUEST----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-nonames.pem python-certbot-0.28.0/certbot/tests/testdata/csr-nonames.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr-nonames.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-nonames.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIH/MIGqAgEAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw -HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEF -AANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+ -6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoAAwDQYJKoZIhvcNAQELBQAD -QQBt9XLSZ9DGfWcGGaBUTCiSY7lWBegpNlCeo8pK3ydWmKpjcza+j7lF5paph2LH -lKWVQ8+xwYMscGWK0NApHGco ------END CERTIFICATE REQUEST----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-nonames_512.pem python-certbot-0.28.0/certbot/tests/testdata/csr-nonames_512.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr-nonames_512.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-nonames_512.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIH/MIGqAgEAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEF +AANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+ +6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoAAwDQYJKoZIhvcNAQELBQAD +QQBt9XLSZ9DGfWcGGaBUTCiSY7lWBegpNlCeo8pK3ydWmKpjcza+j7lF5paph2LH +lKWVQ8+xwYMscGWK0NApHGco +-----END CERTIFICATE REQUEST----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-nosans.pem python-certbot-0.28.0/certbot/tests/testdata/csr-nosans.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr-nosans.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-nosans.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh -MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt -cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn -BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz -AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo -wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= ------END CERTIFICATE REQUEST----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-nosans_512.conf python-certbot-0.28.0/certbot/tests/testdata/csr-nosans_512.conf --- python-certbot-0.10.2/certbot/tests/testdata/csr-nosans_512.conf 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-nosans_512.conf 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,16 @@ +[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 \ No newline at end of file diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-nosans_512.pem python-certbot-0.28.0/certbot/tests/testdata/csr-nosans_512.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr-nosans_512.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-nosans_512.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBMzCB3gIBADB5MQswCQYDVQQGEwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQ +BgNVBAcMCUFubiBBcmJvcjEMMAoGA1UECgwDRUZGMR8wHQYDVQQLDBZVbml2ZXJz +aXR5IHBmIE1pY2hpZ2FuMRQwEgYDVQQDDAtleGFtcGxlLmNvbTBcMA0GCSqGSIb3 +DQEBAQUAA0sAMEgCQQCsdXO0Ue0f3a5wUkP838db0Cx1GxS4dQEEEOUfA2VF3d+n +nzSu/b7pBYTfRxaB2YlLzo5tHPqVROivhHRP7cLlAgMBAAGgADANBgkqhkiG9w0B +AQsFAANBAG06jIPvSC6wiGLy7sUTaEX4UCE6Cztp3vh/uXN7Q++CGn6KiXNs/BRW +eFlcFPbvxbVG/ZZFR5aPs+Oy6RhqOjg= +-----END CERTIFICATE REQUEST----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-nosans_nistp256.pem python-certbot-0.28.0/certbot/tests/testdata/csr-nosans_nistp256.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr-nosans_nistp256.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-nosans_nistp256.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBFDCBugIBADBYMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQ +BgNVBAcMCUFubiBBcmJvcjEMMAoGA1UECgwDRUZGMRQwEgYDVQQDDAtleGFtcGxl +LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDz5dCWrkmbpLwFFr+JeVTSw +GEF4bj12EAX+HVRE97NBOavWQm0nvqTVG5KPRfkxZLnO11Y0D7H5A24dCPZw9Leg +ADAKBggqhkjOPQQDAgNJADBGAiEAuoZHrYA5sy2DRTdLAxJTBNHKFFKbtaGt+QaJ +A62qa8sCIQCUkSgSAiNaEnJ7r5fKphdjeORHqhpl6flYkLE3lGmGdg== +-----END CERTIFICATE REQUEST----- Binary files /srv/release.debian.org/tmp/U7pNdq3mJD/python-certbot-0.10.2/certbot/tests/testdata/csr-san.der and /srv/release.debian.org/tmp/5tQRbMTy6D/python-certbot-0.28.0/certbot/tests/testdata/csr-san.der differ diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-san.pem python-certbot-0.28.0/certbot/tests/testdata/csr-san.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr-san.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-san.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw -EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy -c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG -9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f -p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN -AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t -MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy -tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== ------END CERTIFICATE REQUEST----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr-san_512.pem python-certbot-0.28.0/certbot/tests/testdata/csr-san_512.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr-san_512.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr-san_512.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw +EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy +c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG +9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f +p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN +AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t +MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy +tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== +-----END CERTIFICATE REQUEST----- Binary files /srv/release.debian.org/tmp/U7pNdq3mJD/python-certbot-0.10.2/certbot/tests/testdata/csr.der and /srv/release.debian.org/tmp/5tQRbMTy6D/python-certbot-0.28.0/certbot/tests/testdata/csr.der differ diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr.pem python-certbot-0.28.0/certbot/tests/testdata/csr.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw -EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy -c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG -9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f -p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN -AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB -AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G -n9XBE1N9W6HCIEut2d8wACg= ------END CERTIFICATE REQUEST----- Binary files /srv/release.debian.org/tmp/U7pNdq3mJD/python-certbot-0.10.2/certbot/tests/testdata/csr_512.der and /srv/release.debian.org/tmp/5tQRbMTy6D/python-certbot-0.28.0/certbot/tests/testdata/csr_512.der differ diff -Nru python-certbot-0.10.2/certbot/tests/testdata/csr_512.pem python-certbot-0.28.0/certbot/tests/testdata/csr_512.pem --- python-certbot-0.10.2/certbot/tests/testdata/csr_512.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/csr_512.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh +MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtFeGFt +cGxlLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCsdXO0Ue0f3a5wUkP838db +0Cx1GxS4dQEEEOUfA2VF3d+nnzSu/b7pBYTfRxaB2YlLzo5tHPqVROivhHRP7cLl +AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAAceUlq4La8qaiK0DeDP3M19BIVzMmz2 +oemG2fOvPiwNCB90ctSWQ6bMpUMV85ShcFi31C5vlntPfztehhq6YuE= +-----END CERTIFICATE REQUEST----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/dsa512_key.pem python-certbot-0.28.0/certbot/tests/testdata/dsa512_key.pem --- python-certbot-0.10.2/certbot/tests/testdata/dsa512_key.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/dsa512_key.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ ------BEGIN DSA PARAMETERS----- -MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC -OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA -qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl -41pgNJpgu99YOYqPpS0g7A== ------END DSA PARAMETERS----- ------BEGIN DSA PRIVATE KEY----- -MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf -n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP -AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm -rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth -zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE -mNv063So6E+eYaIN ------END DSA PRIVATE KEY----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/dsa_cert.pem python-certbot-0.28.0/certbot/tests/testdata/dsa_cert.pem --- python-certbot-0.10.2/certbot/tests/testdata/dsa_cert.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/dsa_cert.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICuDCCAnWgAwIBAgIJAPjmErVMzwVLMAsGCWCGSAFlAwQDAjB3MQswCQYDVQQG -EwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQBgNVBAcMCUFubiBBcmJvcjErMCkG -A1UECgwiVW5pdmVyc2l0eSBvZiBNaWNoaWdhbiBhbmQgdGhlIEVGRjEUMBIGA1UE -AwwLZXhhbXBsZS5jb20wHhcNMTUwNTEyMTUzOTQzWhcNMTUwNjExMTUzOTQzWjB3 -MQswCQYDVQQGEwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQBgNVBAcMCUFubiBB -cmJvcjErMCkGA1UECgwiVW5pdmVyc2l0eSBvZiBNaWNoaWdhbiBhbmQgdGhlIEVG -RjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgfEwgakGByqGSM44BAEwgZ0CQQDB5sSg -YF+iQpB4AscecBkxDBhTfkgsQF1XyhSbO/uqlJVSgeKHKp+foYI6LEApI/wQlhxO -KUio9sVt8XI4+VsvAhUA2gUcQOJCCScC8qsbvykfMAl1BI8CQQCp+RrkGeX4J4Qy -nNVkas5WpkT8sV1kr15Ppi1aPOq0iR/eHBdRXEmxOcEbjGat++XjWmA0mmC731g5 -io+lLSDsA0MAAkBNDYtTOMZBIzpSWNw9jkjY4P1MeRRH2Qfa22HNl3vRSgj1u2tV -pOLOCphKG6iT3iCVJA0rQf3YmBSTexwk9oCQo1AwTjAdBgNVHQ4EFgQUZ2DlTDGU -PMwTUt0KztM6IyX61BcwHwYDVR0jBBgwFoAUZ2DlTDGUPMwTUt0KztM6IyX61Bcw -DAYDVR0TBAUwAwEB/zALBglghkgBZQMEAwIDMAAwLQIVAIbMgGx+KwBr4rgqZ2Lh -AAO8TegHAhQsuxpIIIphiReoWEtEJk4TqEIz/A== ------END CERTIFICATE----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/matching_cert.pem python-certbot-0.28.0/certbot/tests/testdata/matching_cert.pem --- python-certbot-0.10.2/certbot/tests/testdata/matching_cert.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/matching_cert.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICNzCCAeGgAwIBAgIJALizm9Y3q620MA0GCSqGSIb3DQEBCwUAMHcxCzAJBgNV -BAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjESMBAGA1UEBwwJQW5uIEFyYm9yMSsw -KQYDVQQKDCJVbml2ZXJzaXR5IG9mIE1pY2hpZ2FuIGFuZCB0aGUgRUZGMRQwEgYD -VQQDDAtleGFtcGxlLmNvbTAeFw0xNTA1MDkwMDI0NTJaFw0xNjA1MDgwMDI0NTJa -MHcxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjESMBAGA1UEBwwJQW5u -IEFyYm9yMSswKQYDVQQKDCJVbml2ZXJzaXR5IG9mIE1pY2hpZ2FuIGFuZCB0aGUg -RUZGMRQwEgYDVQQDDAtleGFtcGxlLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC -QQD0thFxUTc2v6qV55wRxfwnBUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3Hg -BVy9ddFc8RX4vNZaR+ROXNEzAgMBAAGjUDBOMB0GA1UdDgQWBBRJieHEVSHKmBk0 -mTExx1erzlylCjAfBgNVHSMEGDAWgBRJieHEVSHKmBk0mTExx1erzlylCjAMBgNV -HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA0EABT/nlpqOaanFSLZmWIrKv0zt63k4 -bmWNMA8fYT45KYpLomsW8qXdpC82IlVKfNk7fW0UYT3HOeDSJRcycxNCTQ== ------END CERTIFICATE----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/nistp256_key.pem python-certbot-0.28.0/certbot/tests/testdata/nistp256_key.pem --- python-certbot-0.10.2/certbot/tests/testdata/nistp256_key.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/nistp256_key.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIOvXH384CyNNv2lfxvjc7hg2f7ScYoLvlk/VpINLJlGBoAoGCCqGSM49 +AwEHoUQDQgAEPPl0JauSZukvAUWv4l5VNLAYQXhuPXYQBf4dVET3s0E5q9ZCbSe+ +pNUbko9F+TFkuc7XVjQPsfkDbh0I9nD0tw== +-----END EC PRIVATE KEY----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/rsa2048_key.pem python-certbot-0.28.0/certbot/tests/testdata/rsa2048_key.pem --- python-certbot-0.10.2/certbot/tests/testdata/rsa2048_key.pem 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/rsa2048_key.pem 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,28 @@ +-----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----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/rsa512_key_2.pem python-certbot-0.28.0/certbot/tests/testdata/rsa512_key_2.pem --- python-certbot-0.10.2/certbot/tests/testdata/rsa512_key_2.pem 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/rsa512_key_2.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIBOwIBAAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNHtPXJmLlM -8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAQJBALmppYQ/JVARjWBcsEm/ -1/bXBJ127YLv4gQIY5baL4r6IdEE33OXMTTmD9wf+ajuq1eaH0htHkwhOvREu0sz -bskCIQD/Cg+xhEVLcwK3pFp3afPIhj1IPFiL3Uy/nqyMZ6O/RQIhAPWiDBofp7Cp -J4dGZs+hkRySq/IOeeRJlNK1Pq64nToXAiBZ7+te1100YSd5KT051SRB94zO13EG -SZESFduVW8rz3QIgK+tLiqg6TYYRQUi/PUTAM4GuKNuZw828RGiPyqHLywUCIQCd -pkZrNphL/y0D7HSbPIfZzD90M2V8tUjlK0BTqk1bHA== ------END RSA PRIVATE KEY----- diff -Nru python-certbot-0.10.2/certbot/tests/testdata/sample-renewal.conf python-certbot-0.28.0/certbot/tests/testdata/sample-renewal.conf --- python-certbot-0.10.2/certbot/tests/testdata/sample-renewal.conf 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/testdata/sample-renewal.conf 2018-11-07 21:14:56.000000000 +0000 @@ -61,7 +61,7 @@ break_my_certs = False standalone = True manual = False -server = https://acme-staging.api.letsencrypt.org/directory +server = https://acme-staging-v02.api.letsencrypt.org/directory standalone_supported_challenges = "tls-sni-01,http-01" webroot = False os_packages_only = False diff -Nru python-certbot-0.10.2/certbot/tests/util.py python-certbot-0.28.0/certbot/tests/util.py --- python-certbot-0.10.2/certbot/tests/util.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/util.py 2018-11-07 21:14:56.000000000 +0000 @@ -3,23 +3,28 @@ .. warning:: This module is not part of the public API. """ +import multiprocessing import os import pkg_resources import shutil +import tempfile import unittest +import sys +import warnings from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import mock import OpenSSL - -from acme import errors -from acme import jose -from acme import util +import josepy as jose +import six +from six.moves import reload_module # pylint: disable=import-error from certbot import constants from certbot import interfaces from certbot import storage +from certbot import util +from certbot import configuration from certbot.display import util as display_util @@ -33,8 +38,15 @@ def load_vector(*names): """Load contents of a test vector.""" # luckily, resource_string opens file in binary mode - return pkg_resources.resource_string( + data = pkg_resources.resource_string( __name__, os.path.join('testdata', *names)) + # 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, loader_pem, loader_der): @@ -54,11 +66,6 @@ return OpenSSL.crypto.load_certificate(loader, load_vector(*names)) -def load_comparable_cert(*names): - """Load ComparableX509 cert.""" - return jose.ComparableX509(load_cert(*names)) - - def load_csr(*names): """Load certificate request.""" loader = _guess_loader( @@ -86,20 +93,6 @@ return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) -def requirement_available(requirement): - """Checks if requirement can be imported. - - :rtype: bool - :returns: ``True`` iff requirement can be imported - - """ - try: - util.activate(requirement) - except errors.DependencyError: # pragma: no cover - return False - return True # pragma: no cover - - def skip_unless(condition, reason): # pragma: no cover """Skip tests unless a condition holds. @@ -121,12 +114,13 @@ return lambda cls: None -def make_lineage(self, testfile): +def make_lineage(config_dir, testfile): """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 :returns: path to the renewal conf file for the created lineage @@ -136,11 +130,11 @@ lineage_name = testfile[:-len('.conf')] conf_dir = os.path.join( - self.config_dir, constants.RENEWAL_CONFIGS_DIR) + config_dir, constants.RENEWAL_CONFIGS_DIR) archive_dir = os.path.join( - self.config_dir, constants.ARCHIVE_DIR, lineage_name) + config_dir, constants.ARCHIVE_DIR, lineage_name) live_dir = os.path.join( - self.config_dir, constants.LIVE_DIR, lineage_name) + config_dir, constants.LIVE_DIR, lineage_name) for directory in (archive_dir, conf_dir, live_dir,): if not os.path.exists(directory): @@ -155,11 +149,11 @@ 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(self.config_dir, conf_dir, testfile) + 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', self.config_dir) for line in src) + line.replace('MAGICDIR', config_dir) for line in src) return conf_path @@ -179,12 +173,36 @@ return mock.patch(target, new_callable=_create_get_utility_mock) +def patch_get_utility_with_stdout(target='zope.component.getUtility', + stdout=None): + """Patch zope.component.getUtility to use a special mock IDisplay. + + The mock IDisplay works like a regular mock object, except it also + also asserts that methods are called with valid arguments. + + The `message` argument passed to the IDisplay methods is passed to + stdout's write method. + + :param str target: path to patch + :param object stdout: object to write standard output to; it is + expected to have a `write` method + + :returns: mock zope.component.getUtility + :rtype: mock.MagicMock + + """ + stdout = stdout if stdout else six.StringIO() + + freezable_mock = _create_get_utility_mock_with_stdout(stdout) + return mock.patch(target, new=freezable_mock) + + class FreezableMock(object): """Mock object with the ability to freeze attributes. This class works like a regular mock.MagicMock object, except - attributes and behavior can be set and frozen so they cannot be - changed during tests. + 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, @@ -192,10 +210,12 @@ value of func is ignored. """ - def __init__(self, frozen=False, func=None): + def __init__(self, frozen=False, func=None, return_value=mock.sentinel.DEFAULT): self._frozen_set = set() if frozen else set(('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): @@ -213,17 +233,38 @@ 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, value): + """ 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: - return setattr(self._mock, name, value) - elif name != '_frozen_set': + if name in self._frozen_set: + raise AttributeError('Cannot change frozen attribute ' + name) + else: + return setattr(self._mock, name, value) + + if name != '_frozen_set': self._frozen_set.add(name) - return object.__setattr__(self, name, value) + + if name in ('return_value', 'side_effect'): + return setattr(self._mock, name, value) + + else: + return object.__setattr__(self, name, value) def _create_get_utility_mock(): @@ -233,7 +274,37 @@ frozen_mock = FreezableMock(frozen=True, func=_assert_valid_call) setattr(display, name, frozen_mock) display.freeze() - return mock.MagicMock(return_value=display) + return FreezableMock(frozen=True, return_value=display) + + +def _create_get_utility_mock_with_stdout(stdout): + def _write_msg(message, *unused_args, **unused_kwargs): + """Write to message to stdout. + """ + if message: + stdout.write(message) + + def mock_method(*args, **kwargs): + """ + Mock function for IDisplay methods. + """ + _assert_valid_call(args, kwargs) + _write_msg(*args, **kwargs) + + + display = FreezableMock() + for name in interfaces.IDisplay.names(): # pylint: disable=no-member + if name == 'notification': + frozen_mock = FreezableMock(frozen=True, + func=_write_msg) + setattr(display, name, frozen_mock) + else: + frozen_mock = FreezableMock(frozen=True, + func=mock_method) + setattr(display, name, frozen_mock) + display.freeze() + + return FreezableMock(frozen=True, return_value=display) def _assert_valid_call(*args, **kwargs): @@ -246,3 +317,106 @@ # pylint: disable=star-args 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): + """Execute before test""" + self.tempdir = tempfile.mkdtemp() + + def tearDown(self): + """Execute after test""" + # Then we have various files which are not correctly closed at the time of tearDown. + # On Windows, it is visible for the same reasons as above. + # For know, we log them until a proper file close handling is written. + def onerror_handler(_, path, excinfo): + """On error handler""" + message = ('Following error occurred when deleting the tempdir {0}' + ' for path {1} during tearDown process: {2}' + .format(self.tempdir, path, str(excinfo))) + warnings.warn(message) + shutil.rmtree(self.tempdir, onerror=onerror_handler) + +class ConfigTestCase(TempDirTestCase): + """Test class which sets up a NamespaceConfig object. + + """ + def setUp(self): + super(ConfigTestCase, self).setUp() + self.config = configuration.NamespaceConfig( + mock.MagicMock(**constants.CLI_DEFAULTS) + ) + self.config.verb = "certonly" + self.config.config_dir = os.path.join(self.tempdir, 'config') + self.config.work_dir = os.path.join(self.tempdir, 'work') + self.config.logs_dir = os.path.join(self.tempdir, 'logs') + self.config.cert_path = constants.CLI_DEFAULTS['auth_cert_path'] + self.config.fullchain_path = constants.CLI_DEFAULTS['auth_chain_path'] + self.config.chain_path = constants.CLI_DEFAULTS['auth_chain_path'] + self.config.server = "https://example.com" + +def lock_and_call(func, lock_path): + """Grab a lock for lock_path and call func. + + :param callable func: object to call after acquiring the lock + :param str lock_path: path to file or directory to lock + + """ + # Reload module to reset internal _LOCKS dictionary + reload_module(util) + + # start child and wait for it to grab the lock + cv = multiprocessing.Condition() + cv.acquire() + child_args = (cv, lock_path,) + child = multiprocessing.Process(target=hold_lock, args=child_args) + child.start() + cv.wait() + + # call func and terminate the child + func() + cv.notify() + cv.release() + child.join() + assert child.exitcode == 0 + +def hold_lock(cv, lock_path): # pragma: no cover + """Acquire a file lock at lock_path and wait to release it. + + :param multiprocessing.Condition cv: condition for synchronization + :param str lock_path: path to the file lock + + """ + from certbot import lock + if os.path.isdir(lock_path): + my_lock = lock.lock_dir(lock_path) + else: + my_lock = lock.LockFile(lock_path) + cv.acquire() + cv.notify() + cv.wait() + my_lock.release() + +def skip_on_windows(reason): + """Decorator to skip permanently a test on Windows. A reason is required.""" + def wrapper(function): + """Wrapped version""" + return unittest.skipIf(sys.platform == 'win32', reason)(function) + return wrapper + +def broken_on_windows(function): + """Decorator to skip temporarily a broken test on Windows.""" + reason = 'Test is broken and ignored on windows but should be fixed.' + return unittest.skipIf( + sys.platform == 'win32' + and os.environ.get('SKIP_BROKEN_TESTS_ON_WINDOWS', 'true') == 'true', + reason)(function) + +def temp_join(path): + """ + 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) diff -Nru python-certbot-0.10.2/certbot/tests/util_test.py python-certbot-0.28.0/certbot/tests/util_test.py --- python-certbot-0.10.2/certbot/tests/util_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/tests/util_test.py 2018-11-07 21:14:56.000000000 +0000 @@ -3,13 +3,13 @@ import errno import os import shutil -import stat -import tempfile import unittest import mock import six +from six.moves import reload_module # pylint: disable=import-error +from certbot import compat from certbot import errors import certbot.tests.util as test_util @@ -75,7 +75,60 @@ self.assertFalse(self._call("exe")) -class MakeOrVerifyDirTest(unittest.TestCase): +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(LockDirUntilExit, self).setUp() + # reset global state from other tests + import certbot.util + reload_module(certbot.util) + + @test_util.broken_on_windows + @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') + os.mkdir(subdir) + self._call(self.tempdir) + self._call(subdir) + self._call(subdir) + + self.assertEqual(mock_register.call_count, 1) + registered_func = mock_register.call_args[0][0] + shutil.rmtree(subdir) + registered_func() # exception not raised + # logger.debug is only called once because the second call + # to lock subdir was ignored because it was already locked + self.assertEqual(mock_logger.debug.call_count, 1) + + +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, compat.os_geteuid(), False) + self.assertTrue(os.path.exists(new_dir)) + self.assertEqual(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 + self.assertRaises(errors.Error, self._call, + self.tempdir, 0o700, compat.os_geteuid(), 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, @@ -84,31 +137,30 @@ """ def setUp(self): - self.root_path = tempfile.mkdtemp() - self.path = os.path.join(self.root_path, "foo") - os.mkdir(self.path, 0o400) + super(MakeOrVerifyDirTest, self).setUp() - self.uid = os.getuid() + self.path = os.path.join(self.tempdir, "foo") + os.mkdir(self.path, 0o600) - def tearDown(self): - shutil.rmtree(self.root_path, ignore_errors=True) + self.uid = compat.os_geteuid() def _call(self, directory, mode): from certbot.util import make_or_verify_dir return make_or_verify_dir(directory, mode, self.uid, strict=True) def test_creates_dir_when_missing(self): - path = os.path.join(self.root_path, "bar") + path = os.path.join(self.tempdir, "bar") self._call(path, 0o650) self.assertTrue(os.path.isdir(path)) - self.assertEqual(stat.S_IMODE(os.stat(path).st_mode), 0o650) + self.assertTrue(compat.compare_file_modes(os.stat(path).st_mode, 0o650)) def test_existing_correct_mode_does_not_fail(self): - self._call(self.path, 0o400) - self.assertEqual(stat.S_IMODE(os.stat(self.path).st_mode), 0o400) + self._call(self.path, 0o600) + self.assertTrue(compat.compare_file_modes(os.stat(self.path).st_mode, 0o600)) + @test_util.skip_on_windows('Umask modes are mostly ignored on Windows.') def test_existing_wrong_mode_fails(self): - self.assertRaises(errors.Error, self._call, self.path, 0o600) + self.assertRaises(errors.Error, self._call, self.path, 0o400) def test_reraises_os_error(self): with mock.patch.object(os, "makedirs") as makedirs: @@ -116,7 +168,7 @@ self.assertRaises(OSError, self._call, "bar", 12312312) -class CheckPermissionsTest(unittest.TestCase): +class CheckPermissionsTest(test_util.TempDirTestCase): """Tests for certbot.util.check_permissions. Note that it is not possible to test for a wrong file owner, @@ -125,34 +177,30 @@ """ def setUp(self): - _, self.path = tempfile.mkstemp() - self.uid = os.getuid() + super(CheckPermissionsTest, self).setUp() - def tearDown(self): - os.remove(self.path) + self.uid = compat.os_geteuid() def _call(self, mode): from certbot.util import check_permissions - return check_permissions(self.path, mode, self.uid) + return check_permissions(self.tempdir, mode, self.uid) def test_ok_mode(self): - os.chmod(self.path, 0o600) + os.chmod(self.tempdir, 0o600) self.assertTrue(self._call(0o600)) def test_wrong_mode(self): - os.chmod(self.path, 0o400) + os.chmod(self.tempdir, 0o400) self.assertFalse(self._call(0o600)) -class UniqueFileTest(unittest.TestCase): +class UniqueFileTest(test_util.TempDirTestCase): """Tests for certbot.util.unique_file.""" def setUp(self): - self.root_path = tempfile.mkdtemp() - self.default_name = os.path.join(self.root_path, "foo.txt") + super(UniqueFileTest, self).setUp() - def tearDown(self): - shutil.rmtree(self.root_path, ignore_errors=True) + self.default_name = os.path.join(self.tempdir, "foo.txt") def _call(self, mode=0o600): from certbot.util import unique_file @@ -165,8 +213,8 @@ self.assertEqual(open(name).read(), "bar") def test_right_mode(self): - self.assertEqual(0o700, os.stat(self._call(0o700)[1]).st_mode & 0o777) - self.assertEqual(0o100, os.stat(self._call(0o100)[1]).st_mode & 0o777) + self.assertTrue(compat.compare_file_modes(0o700, os.stat(self._call(0o700)[1]).st_mode)) + self.assertTrue(compat.compare_file_modes(0o600, os.stat(self._call(0o600)[1]).st_mode)) def test_default_exists(self): name1 = self._call()[1] # create 0000_foo.txt @@ -177,9 +225,9 @@ self.assertNotEqual(name1, name3) self.assertNotEqual(name2, name3) - self.assertEqual(os.path.dirname(name1), self.root_path) - self.assertEqual(os.path.dirname(name2), self.root_path) - self.assertEqual(os.path.dirname(name3), self.root_path) + self.assertEqual(os.path.dirname(name1), self.tempdir) + self.assertEqual(os.path.dirname(name2), self.tempdir) + self.assertEqual(os.path.dirname(name3), self.tempdir) basename1 = os.path.basename(name2) self.assertTrue(basename1.endswith("foo.txt")) @@ -193,32 +241,26 @@ file_type = file except NameError: import io - file_type = io.TextIOWrapper + file_type = io.TextIOWrapper # type: ignore -class UniqueLineageNameTest(unittest.TestCase): +class UniqueLineageNameTest(test_util.TempDirTestCase): """Tests for certbot.util.unique_lineage_name.""" - def setUp(self): - self.root_path = tempfile.mkdtemp() - - def tearDown(self): - shutil.rmtree(self.root_path, ignore_errors=True) - def _call(self, filename, mode=0o777): from certbot.util import unique_lineage_name - return unique_lineage_name(self.root_path, filename, mode) + return unique_lineage_name(self.tempdir, filename, mode) def test_basic(self): f, path = self._call("wow") self.assertTrue(isinstance(f, file_type)) - self.assertEqual(os.path.join(self.root_path, "wow.conf"), path) + self.assertEqual(os.path.join(self.tempdir, "wow.conf"), path) def test_multiple(self): for _ in six.moves.range(10): f, name = self._call("wow") self.assertTrue(isinstance(f, file_type)) - self.assertTrue(isinstance(name, str)) + self.assertTrue(isinstance(name, six.string_types)) self.assertTrue("wow-0009.conf" in name) @mock.patch("certbot.util.os.fdopen") @@ -237,15 +279,13 @@ self.assertRaises(OSError, self._call, "wow") -class SafelyRemoveTest(unittest.TestCase): +class SafelyRemoveTest(test_util.TempDirTestCase): """Tests for certbot.util.safely_remove.""" def setUp(self): - self.tmp = tempfile.mkdtemp() - self.path = os.path.join(self.tmp, "foo") + super(SafelyRemoveTest, self).setUp() - def tearDown(self): - shutil.rmtree(self.tmp) + self.path = os.path.join(self.tempdir, "foo") def _call(self): from certbot.util import safely_remove @@ -330,6 +370,30 @@ pass self.assertTrue("--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) + self.assertEqual( + len(mock_configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE), 1) + class EnforceLeValidity(unittest.TestCase): """Test enforce_le_validity.""" @@ -358,6 +422,13 @@ def test_valid_domain(self): self.assertEqual(self._call(u"example.com"), u"example.com") + def test_input_with_scheme(self): + self.assertRaises(errors.ConfigurationError, self._call, u"http://example.com") + self.assertRaises(errors.ConfigurationError, self._call, u"https://example.com") + + def test_valid_input_with_scheme_name(self): + self.assertEqual(self._call(u"http.example.com"), u"http.example.com") + class EnforceDomainSanityTest(unittest.TestCase): """Test enforce_domain_sanity.""" @@ -418,22 +489,41 @@ 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): + self.assertFalse(self._call(self.no_wildcard)) + self.assertFalse(self._call(self.no_wildcard.encode())) + + def test_wildcard(self): + self.assertTrue(self._call(self.wildcard)) + self.assertTrue(self._call(self.wildcard.encode())) + + class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" def test_systemd_os_release(self): from certbot.util import (get_os_info, get_systemd_os_info, - get_os_info_ua) + get_os_info_ua) with mock.patch('os.path.isfile', return_value=True): self.assertEqual(get_os_info( test_util.vector_path("os-release"))[0], 'systemdos') self.assertEqual(get_os_info( test_util.vector_path("os-release"))[1], '42') - self.assertEqual(get_systemd_os_info("/dev/null"), ("", "")) + self.assertEqual(get_systemd_os_info(os.devnull), ("", "")) self.assertEqual(get_os_info_ua( - test_util.vector_path("os-release")), - "SystemdOS") + test_util.vector_path("os-release")), "SystemdOS") with mock.patch('os.path.isfile', return_value=False): self.assertEqual(get_systemd_os_info(), ("", "")) @@ -489,5 +579,37 @@ ("windows", "95")) +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) + self.assertFalse(self.func.called) + + 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) + + # _INITAL_PID must be mocked when calling atexit_func + self.assertTrue(mock_atexit.register.called) + args, kwargs = mock_atexit.register.call_args + atexit_func = args[0] + atexit_func(*args[1:], **kwargs) # pylint: disable=star-args + + if __name__ == "__main__": unittest.main() # pragma: no cover diff -Nru python-certbot-0.10.2/certbot/updater.py python-certbot-0.28.0/certbot/updater.py --- python-certbot-0.10.2/certbot/updater.py 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-0.28.0/certbot/updater.py 2018-11-07 21:14:56.000000000 +0000 @@ -0,0 +1,122 @@ +"""Updaters run at renewal""" +import logging + +from certbot import errors +from certbot import interfaces + +from certbot.plugins import selection as plug_sel +import certbot.plugins.enhancements as enhancements + +logger = logging.getLogger(__name__) + +def run_generic_updaters(config, lineage, plugins): + """Run updaters that the plugin supports + + :param config: Configuration object + :type config: interfaces.IConfig + + :param lineage: Certificate lineage object + :type lineage: storage.RenewableCert + + :param plugins: List of plugins + :type plugins: `list` of `str` + + :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.warning("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, lineage, installer): + """Helper function to run deployer interface method if supported by the used + installer plugin. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param lineage: Certificate lineage object + :type lineage: storage.RenewableCert + + :param installer: Installer object + :type installer: interfaces.IInstaller + + :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, installer, config): + """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.IInstaller + + :returns: `None` + :rtype: None + """ + if not config.disable_renew_updates: + if isinstance(installer, interfaces.GenericUpdater): + installer.generic_updates(lineage) + +def _run_enhancement_updaters(lineage, installer, config): + """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.IInstaller + + :param config: Configuration object + :type config: interfaces.IConfig + """ + + 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, installer, config): + """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.IInstaller + + :param config: Configuration object + :type config: interfaces.IConfig + """ + + 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) diff -Nru python-certbot-0.10.2/certbot/util.py python-certbot-0.28.0/certbot/util.py --- python-certbot-0.10.2/certbot/util.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-0.28.0/certbot/util.py 2018-11-07 21:14:56.000000000 +0000 @@ -1,5 +1,6 @@ """Utilities for all Certbot.""" import argparse +import atexit import collections # distutils.version under virtualenv confuses pylint # For more info, see: https://github.com/PyCQA/pylint/issues/73 @@ -11,14 +12,18 @@ import re import six import socket -import stat import subprocess import sys +from collections import OrderedDict + import configargparse +from acme.magic_typing import Tuple, Union # pylint: disable=unused-import, no-name-in-module +from certbot import compat from certbot import constants from certbot import errors +from certbot import lock logger = logging.getLogger(__name__) @@ -38,6 +43,21 @@ 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 = OrderedDict() # type: OrderedDict[str, lock.LockFile] + + def run_script(params, log=logger.error): """Run the script with the given params. @@ -68,6 +88,18 @@ return stdout, stderr +def is_exe(path): + """Is path an executable file? + + :param str path: path to test + + :returns: True iff path is an executable file + :rtype: bool + + """ + return os.path.isfile(path) and os.access(path, os.X_OK) + + def exe_exists(exe): """Determine whether path/name refers to an executable. @@ -77,10 +109,6 @@ :rtype: bool """ - def is_exe(path): - """Determine if path is an exe.""" - return os.path.isfile(path) and os.access(path, os.X_OK) - path, _ = os.path.split(exe) if path: return is_exe(exe) @@ -92,6 +120,50 @@ return False +def lock_dir_until_exit(dir_path): + """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(): + for dir_lock in six.itervalues(_LOCKS): + 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) + + +def set_up_core_dir(directory, mode, uid, strict): + """Ensure directory exists with proper permissions and is locked. + + :param str directory: Path to a directory. + :param int mode: Directory mode. + :param int uid: Directory owner. + :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, uid, 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, mode=0o755, uid=0, strict=False): """Make sure directory exists with proper permissions. @@ -132,7 +204,7 @@ """ file_stat = os.stat(filepath) - return stat.S_IMODE(file_stat.st_mode) == mode and file_stat.st_uid == uid + return compat.compare_file_modes(file_stat.st_mode, mode) and file_stat.st_uid == uid def safe_open(path, mode="w", chmod=None, buffering=None): @@ -147,8 +219,12 @@ """ # pylint: disable=star-args - open_args = () if chmod is None else (chmod,) - fdopen_args = () if buffering is None else (buffering,) + open_args = () # type: Union[Tuple[()], Tuple[int]] + if chmod is not None: + open_args = (chmod,) + fdopen_args = () # type: Union[Tuple[()], Tuple[int]] + if buffering is not None: + fdopen_args = (buffering,) return os.fdopen( os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, *open_args), mode, *fdopen_args) @@ -219,6 +295,24 @@ raise +def get_filtered_names(all_names): + """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(filepath="/etc/os-release"): """ Get OS name and version @@ -248,9 +342,9 @@ """ if os.path.isfile(filepath): - os_ua = _get_systemd_os_release_var("PRETTY_NAME", filepath=filepath) + os_ua = get_var_from_file("PRETTY_NAME", filepath=filepath) if not os_ua: - os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) + os_ua = get_var_from_file("NAME", filepath=filepath) if os_ua: return os_ua @@ -267,8 +361,8 @@ :rtype: `tuple` of `str` """ - os_name = _get_systemd_os_release_var("ID", filepath=filepath) - os_version = _get_systemd_os_release_var("VERSION_ID", filepath=filepath) + os_name = get_var_from_file("ID", filepath=filepath) + os_version = get_var_from_file("VERSION_ID", filepath=filepath) return (os_name, os_version) @@ -283,10 +377,10 @@ :rtype: `list` of `str` """ - return _get_systemd_os_release_var("ID_LIKE", filepath).split(" ") + return get_var_from_file("ID_LIKE", filepath).split(" ") -def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): +def get_var_from_file(varname, filepath="/etc/os-release"): """ Get single value from systemd /etc/os-release @@ -311,7 +405,7 @@ def _normalize_string(orig): """ - Helper function for _get_systemd_os_release_var() to remove quotes + Helper function for get_var_from_file() to remove quotes and whitespaces """ return orig.replace('"', '').replace("'", "").strip() @@ -341,10 +435,19 @@ if info[1]: os_ver = info[1] elif os_type.startswith('darwin'): - os_ver = subprocess.Popen( - ["sw_vers", "-productVersion"], - stdout=subprocess.PIPE - ).communicate()[0].rstrip('\n') + try: + proc = subprocess.Popen( + ["/usr/bin/sw_vers", "-productVersion"], + stdout=subprocess.PIPE, + universal_newlines=True, + ) + except OSError: + proc = subprocess.Popen( + ["sw_vers", "-productVersion"], + stdout=subprocess.PIPE, + universal_newlines=True, + ) + os_ver = proc.communicate()[0].rstrip('\n') elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] @@ -371,6 +474,13 @@ return False +class _ShowWarning(argparse.Action): + """Action to log a warning when an argument is used.""" + def __call__(self, unused1, unused2, unused3, option_string=None): + sys.stderr.write( + "Use of {0} is deprecated.\n".format(option_string)) + + def add_deprecated_argument(add_argument, argument_name, nargs): """Adds a deprecated argument with the name argument_name. @@ -384,14 +494,17 @@ :param nargs: Value for nargs when adding the argument to argparse. """ - class ShowWarning(argparse.Action): - """Action to log a warning when an argument is used.""" - def __call__(self, unused1, unused2, unused3, option_string=None): - sys.stderr.write( - "Use of {0} is deprecated.\n".format(option_string)) - - configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add(ShowWarning) - add_argument(argument_name, action=ShowWarning, + if _ShowWarning 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): + # pylint: disable=no-member + configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add( + _ShowWarning) + else: + configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE += ( + _ShowWarning,) + add_argument(argument_name, action=_ShowWarning, help=argparse.SUPPRESS, nargs=nargs) @@ -439,16 +552,6 @@ :returns: The domain cast to `str`, with ASCII-only contents :rtype: str """ - if isinstance(domain, six.text_type): - wildcard_marker = u"*." - else: - wildcard_marker = b"*." - - # Check if there's a wildcard domain - if domain.startswith(wildcard_marker): - raise errors.ConfigurationError( - "Wildcard domains are not supported: {0}".format(domain)) - # Unicode try: if isinstance(domain, six.binary_type): @@ -463,6 +566,17 @@ # Remove trailing dot domain = domain[:-1] if domain.endswith(u'.') 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 + ) + ) + # Explain separately that IP addresses aren't allowed (apart from not # being FQDNs) because hope springs eternal concerning this point try: @@ -491,6 +605,24 @@ return domain +def is_wildcard_domain(domain): + """"Is domain a wildcard domain? + + :param domain: domain to check + :type domain: `bytes` or `str` or `unicode` + + :returns: True if domain is a wildcard, otherwise, False + :rtype: bool + + """ + if isinstance(domain, six.text_type): + wildcard_marker = u"*." + else: + wildcard_marker = b"*." + + return domain.startswith(wildcard_marker) + + def get_strict_version(normalized): """Converts a normalized version to a strict version. @@ -514,3 +646,20 @@ :rtype bool: """ return srv == constants.STAGING_URI or "staging" in srv + + +def atexit_register(func, *args, **kwargs): + """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 _atexit_call(func, *args, **kwargs): + if _INITIAL_PID == os.getpid(): + func(*args, **kwargs) diff -Nru python-certbot-0.10.2/certbot.egg-info/PKG-INFO python-certbot-0.28.0/certbot.egg-info/PKG-INFO --- python-certbot-0.10.2/certbot.egg-info/PKG-INFO 2017-01-26 02:58:41.000000000 +0000 +++ python-certbot-0.28.0/certbot.egg-info/PKG-INFO 2018-11-07 21:14:58.000000000 +0000 @@ -1,6 +1,6 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: certbot -Version: 0.10.2 +Version: 0.28.0 Summary: ACME client Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -8,13 +8,13 @@ License: Apache License 2.0 Description: .. 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 - 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 identify 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. + 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, so there’s no need to arrange payment. - How you use Certbot depends on the configuration of your web server. 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. + How you use Certbot depends on the configuration of your web server. 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. - 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 issues by Let’s Encrypt. + Certbot is meant to be run directly on your web server, 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. Certbot is a fully-featured, extensible client for the Let's Encrypt CA (or any other CA that speaks the `ACME @@ -23,6 +23,9 @@ configuring webservers to use them. This client runs on Unix-based operating systems. + To see the changes made to Certbot between versions please refer to our + `changelog `_. + Until May 2016, Certbot was named simply ``letsencrypt`` or ``letsencrypt-auto``, depending on install method. Instructions on the Internet, and some pieces of the software, may still refer to this older name. @@ -96,32 +99,20 @@ Let's Encrypt Website: https://letsencrypt.org - IRC Channel: #letsencrypt on `Freenode`_ or #certbot on `OFTC`_ - Community: https://community.letsencrypt.org ACME spec: http://ietf-wg-acme.github.io/acme/ ACME working area in github: https://github.com/ietf-wg-acme/acme - - Mailing list: `client-dev`_ (to subscribe without a Google account, send an - email to client-dev+subscribe@letsencrypt.org) - |build-status| |coverage| |docs| |container| - .. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt - - .. _OFTC: https://webchat.oftc.net?channels=%23certbot - - .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev - .. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master :target: https://travis-ci.org/certbot/certbot :alt: Travis CI status - .. |coverage| image:: https://coveralls.io/repos/certbot/certbot/badge.svg?branch=master - :target: https://coveralls.io/r/certbot/certbot + .. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg + :target: https://codecov.io/gh/certbot/certbot :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ @@ -137,19 +128,7 @@ System Requirements =================== - The Let's Encrypt Client presently only runs on Unix-ish OSes that include - Python 2.6 or 2.7; Python 3.x support will hopefully be added in the future. The - client requires root access in order to write to ``/etc/letsencrypt``, - ``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to bind to ports 80 and 443 - (if you use the ``standalone`` plugin) and to read and modify webserver - configurations (if you use the ``apache`` or ``nginx`` plugins). If none of - these apply to you, it is theoretically possible to run without root privileges, - but for most users who want to avoid running an ACME client as root, either - `letsencrypt-nosudo `_ or - `simp_le `_ are more appropriate choices. - - The Apache plugin currently requires a Debian-based OS with augeas version - 1.0; this includes Ubuntu 12.04+ and Debian 7+. + See https://certbot.eff.org/docs/install.html#system-requirements. .. Do not modify this comment unless you know what you're doing. tag:intro-end @@ -160,8 +139,8 @@ * Supports multiple web servers: - - apache/2.x (beta support for auto-configuration) - - nginx/0.8.48+ (alpha support for auto-configuration) + - apache/2.x + - nginx/0.8.48+ - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - standalone (runs its own simple webserver to prove you control a domain) @@ -177,7 +156,7 @@ runs https only (Apache only) * Fully automated. * Configuration changes are logged and can be reverted. - * Supports ncurses and text (-t) UI, or can be driven entirely from the + * Supports an interactive text UI, or can be driven entirely from the command line. * Free and Open Source Software, made with Python. @@ -186,7 +165,7 @@ For extensive documentation on using and contributing to Certbot, go to https://certbot.eff.org/docs. If you would like to contribute to the project or run the latest code from git, you should read our `developer guide `_. Platform: UNKNOWN -Classifier: Development Status :: 3 - Alpha +Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: System Administrators @@ -194,11 +173,19 @@ Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 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: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Provides-Extra: dev3 +Provides-Extra: docs +Provides-Extra: dev diff -Nru python-certbot-0.10.2/certbot.egg-info/SOURCES.txt python-certbot-0.28.0/certbot.egg-info/SOURCES.txt --- python-certbot-0.10.2/certbot.egg-info/SOURCES.txt 2017-01-26 02:58:41.000000000 +0000 +++ python-certbot-0.28.0/certbot.egg-info/SOURCES.txt 2018-11-07 21:14:58.000000000 +0000 @@ -1,4 +1,4 @@ -CHANGES.rst +CHANGELOG.md CONTRIBUTING.md LICENSE.txt MANIFEST.in @@ -13,21 +13,26 @@ certbot/cert_manager.py certbot/cli.py certbot/client.py -certbot/colored_logging.py +certbot/compat.py certbot/configuration.py certbot/constants.py certbot/crypto_util.py +certbot/eff.py certbot/error_handler.py certbot/errors.py certbot/hooks.py certbot/interfaces.py +certbot/lock.py +certbot/log.py certbot/main.py certbot/notify.py certbot/ocsp.py certbot/renewal.py certbot/reporter.py certbot/reverter.py +certbot/ssl-dhparams.pem certbot/storage.py +certbot/updater.py certbot/util.py certbot.egg-info/PKG-INFO certbot.egg-info/SOURCES.txt @@ -46,6 +51,14 @@ certbot/plugins/common_test.py certbot/plugins/disco.py certbot/plugins/disco_test.py +certbot/plugins/dns_common.py +certbot/plugins/dns_common_lexicon.py +certbot/plugins/dns_common_lexicon_test.py +certbot/plugins/dns_common_test.py +certbot/plugins/dns_test_common.py +certbot/plugins/dns_test_common_lexicon.py +certbot/plugins/enhancements.py +certbot/plugins/enhancements_test.py certbot/plugins/manual.py certbot/plugins/manual_test.py certbot/plugins/null.py @@ -54,6 +67,8 @@ certbot/plugins/selection_test.py certbot/plugins/standalone.py certbot/plugins/standalone_test.py +certbot/plugins/storage.py +certbot/plugins/storage_test.py certbot/plugins/util.py certbot/plugins/util_test.py certbot/plugins/webroot.py @@ -65,16 +80,20 @@ certbot/tests/cert_manager_test.py certbot/tests/cli_test.py certbot/tests/client_test.py -certbot/tests/colored_logging_test.py +certbot/tests/compat_test.py certbot/tests/configuration_test.py certbot/tests/crypto_util_test.py +certbot/tests/eff_test.py certbot/tests/error_handler_test.py certbot/tests/errors_test.py certbot/tests/hook_test.py +certbot/tests/lock_test.py +certbot/tests/log_test.py certbot/tests/main_test.py certbot/tests/notify_test.py certbot/tests/ocsp_test.py certbot/tests/renewal_test.py +certbot/tests/renewupdater_test.py certbot/tests/reporter_test.py certbot/tests/reverter_test.py certbot/tests/storage_test.py @@ -85,26 +104,29 @@ certbot/tests/display/enhancements_test.py certbot/tests/display/ops_test.py certbot/tests/display/util_test.py -certbot/tests/testdata/cert-5sans.pem -certbot/tests/testdata/cert-san.pem -certbot/tests/testdata/cert.b64jose -certbot/tests/testdata/cert.der -certbot/tests/testdata/cert.pem +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/cli.ini -certbot/tests/testdata/csr-6sans.pem -certbot/tests/testdata/csr-nonames.pem -certbot/tests/testdata/csr-nosans.pem -certbot/tests/testdata/csr-san.der -certbot/tests/testdata/csr-san.pem -certbot/tests/testdata/csr.der -certbot/tests/testdata/csr.pem -certbot/tests/testdata/dsa512_key.pem -certbot/tests/testdata/dsa_cert.pem -certbot/tests/testdata/matching_cert.pem +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/nistp256_key.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/rsa512_key_2.pem certbot/tests/testdata/sample-renewal-ancient.conf certbot/tests/testdata/sample-renewal.conf certbot/tests/testdata/webrootconftest.ini @@ -115,6 +137,7 @@ docs/.gitignore docs/Makefile docs/api.rst +docs/challenges.rst docs/ciphers.rst docs/cli-help.txt docs/conf.py @@ -126,25 +149,41 @@ docs/packaging.rst docs/resources.rst docs/using.rst +docs/what.rst docs/_static/.gitignore +docs/_templates/footer.html docs/api/account.rst docs/api/achallenges.rst docs/api/auth_handler.rst +docs/api/cert_manager.rst +docs/api/cli.rst docs/api/client.rst docs/api/configuration.rst docs/api/constants.rst docs/api/crypto_util.rst docs/api/display.rst +docs/api/eff.rst +docs/api/error_handler.rst docs/api/errors.rst +docs/api/hooks.rst docs/api/index.rst docs/api/interfaces.rst +docs/api/lock.rst +docs/api/log.rst +docs/api/main.rst +docs/api/notify.rst +docs/api/ocsp.rst +docs/api/renewal.rst docs/api/reporter.rst docs/api/reverter.rst docs/api/storage.rst docs/api/util.rst docs/api/plugins/common.rst docs/api/plugins/disco.rst +docs/api/plugins/dns_common.rst +docs/api/plugins/dns_common_lexicon.rst docs/api/plugins/manual.rst +docs/api/plugins/selection.rst docs/api/plugins/standalone.rst docs/api/plugins/util.rst docs/api/plugins/webroot.rst diff -Nru python-certbot-0.10.2/certbot.egg-info/requires.txt python-certbot-0.28.0/certbot.egg-info/requires.txt --- python-certbot-0.10.2/certbot.egg-info/requires.txt 2017-01-26 02:58:41.000000000 +0000 +++ python-certbot-0.28.0/certbot.egg-info/requires.txt 2018-11-07 21:14:58.000000000 +0000 @@ -1,29 +1,33 @@ -acme==0.10.2 +acme>=0.26.0 ConfigArgParse>=0.9.3 configobj -cryptography>=0.7 +cryptography>=1.2 +josepy +mock parsedatetime>=1.3 -PyOpenSSL pyrfc3339 pytz -setuptools>=1.0 -six +setuptools zope.component zope.interface -mock [dev] astroid==1.3.5 coverage -nose -pep8 -psutil>=2.2.1 +ipdb +pytest +pytest-cov +pytest-xdist pylint==1.4.2 tox twine wheel +[dev3] +mypy +typing + [docs] repoze.sphinx.autointerface -Sphinx>=1.0 +Sphinx>=1.6 sphinx_rtd_theme diff -Nru python-certbot-0.10.2/debian/certbot.cron.d python-certbot-0.28.0/debian/certbot.cron.d --- python-certbot-0.10.2/debian/certbot.cron.d 2017-01-19 05:08:30.000000000 +0000 +++ python-certbot-0.28.0/debian/certbot.cron.d 2018-09-14 21:56:50.000000000 +0000 @@ -5,7 +5,13 @@ # Eventually, this will be an opportunity to validate certificates # haven't been revoked, etc. Renewal will only occur if expiration # is within 30 days. +# +# Important Note! This cronjob will NOT be executed if you are +# running systemd as your init system. If you are running systemd, +# the cronjob.timer function takes precedence over this cronjob. For +# more details, see the systemd.timer manpage, or use systemctl show +# certbot.timer. SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin