diff --git a/.azure-pipelines/templates/jobs/extended-tests-jobs.yml b/.azure-pipelines/templates/jobs/extended-tests-jobs.yml index c22f1003f0a..0c92136e801 100644 --- a/.azure-pipelines/templates/jobs/extended-tests-jobs.yml +++ b/.azure-pipelines/templates/jobs/extended-tests-jobs.yml @@ -4,7 +4,7 @@ jobs: - name: IMAGE_NAME value: ubuntu-18.04 - name: PYTHON_VERSION - value: 3.8 + value: 3.9 - group: certbot-common strategy: matrix: @@ -14,6 +14,9 @@ jobs: linux-py37: PYTHON_VERSION: 3.7 TOXENV: py37 + linux-py38: + PYTHON_VERSION: 3.8 + TOXENV: py38 linux-py37-nopin: PYTHON_VERSION: 3.7 TOXENV: py37 @@ -62,10 +65,20 @@ jobs: PYTHON_VERSION: 3.8 TOXENV: integration ACME_SERVER: boulder-v2 + linux-boulder-v1-py39-integration: + PYTHON_VERSION: 3.9 + TOXENV: integration + ACME_SERVER: boulder-v1 + linux-boulder-v2-py39-integration: + PYTHON_VERSION: 3.9 + TOXENV: integration + ACME_SERVER: boulder-v2 nginx-compat: TOXENV: nginx_compat - le-auto-oraclelinux6: - TOXENV: le_auto_oraclelinux6 + linux-integration-rfc2136: + IMAGE_NAME: ubuntu-18.04 + PYTHON_VERSION: 3.8 + TOXENV: integration-dns-rfc2136 docker-dev: TOXENV: docker_dev macos-farmtest-apache2: diff --git a/.azure-pipelines/templates/jobs/packaging-jobs.yml b/.azure-pipelines/templates/jobs/packaging-jobs.yml index f0c6b6e4984..900be9b2f12 100644 --- a/.azure-pipelines/templates/jobs/packaging-jobs.yml +++ b/.azure-pipelines/templates/jobs/packaging-jobs.yml @@ -144,7 +144,7 @@ jobs: git config --global user.name "$(Build.RequestedFor)" mkdir -p ~/.local/share/snapcraft/provider/launchpad cp $(credentials.secureFilePath) ~/.local/share/snapcraft/provider/launchpad/credentials - python3 tools/snap/build_remote.py ALL --archs ${ARCHS} + python3 tools/snap/build_remote.py ALL --archs ${ARCHS} --timeout 19800 displayName: Build snaps - script: | set -e diff --git a/.azure-pipelines/templates/jobs/standard-tests-jobs.yml b/.azure-pipelines/templates/jobs/standard-tests-jobs.yml index 7a1f620bbbe..39cd628bc7e 100644 --- a/.azure-pipelines/templates/jobs/standard-tests-jobs.yml +++ b/.azure-pipelines/templates/jobs/standard-tests-jobs.yml @@ -1,17 +1,17 @@ jobs: - job: test variables: - PYTHON_VERSION: 3.8 + PYTHON_VERSION: 3.9 strategy: matrix: macos-py27: IMAGE_NAME: macOS-10.15 PYTHON_VERSION: 2.7 TOXENV: py27 - macos-py38: + macos-py39: IMAGE_NAME: macOS-10.15 - PYTHON_VERSION: 3.8 - TOXENV: py38 + PYTHON_VERSION: 3.9 + TOXENV: py39 windows-py36: IMAGE_NAME: vs2017-win2016 PYTHON_VERSION: 3.6 @@ -38,10 +38,10 @@ jobs: IMAGE_NAME: ubuntu-18.04 PYTHON_VERSION: 3.6 TOXENV: py36 - linux-py38-cover: + linux-py39-cover: IMAGE_NAME: ubuntu-18.04 - PYTHON_VERSION: 3.8 - TOXENV: py38-cover + PYTHON_VERSION: 3.9 + TOXENV: py39-cover linux-py37-lint: IMAGE_NAME: ubuntu-18.04 PYTHON_VERSION: 3.7 @@ -58,9 +58,9 @@ jobs: apache-compat: IMAGE_NAME: ubuntu-18.04 TOXENV: apache_compat - le-auto-centos6: + le-modification: IMAGE_NAME: ubuntu-18.04 - TOXENV: le_auto_centos6 + TOXENV: modification apacheconftest: IMAGE_NAME: ubuntu-18.04 PYTHON_VERSION: 2.7 diff --git a/AUTHORS.md b/AUTHORS.md index 56fe2a3d89e..f76c323a572 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -154,6 +154,7 @@ Authors * [Luca Olivetti](https://github.com/olivluca) * [Luke Rogers](https://github.com/lukeroge) * [Maarten](https://github.com/mrtndwrd) +* [Mads Jensen](https://github.com/atombrella) * [Maikel Martens](https://github.com/krukas) * [Malte Janduda](https://github.com/MalteJ) * [Mantas Mikulėnas](https://github.com/grawity) @@ -213,6 +214,7 @@ Authors * [Richard Barnes](https://github.com/r-barnes) * [Richard Panek](https://github.com/kernelpanek) * [Robert Buchholz](https://github.com/rbu) +* [Robert Dailey](https://github.com/pahrohfit) * [Robert Habermann](https://github.com/frennkie) * [Robert Xiao](https://github.com/nneonneo) * [Roland Shoemaker](https://github.com/rolandshoemaker) diff --git a/acme/acme/__init__.py b/acme/acme/__init__.py index d1679fcad79..3ec5203bfac 100644 --- a/acme/acme/__init__.py +++ b/acme/acme/__init__.py @@ -20,3 +20,10 @@ # preserved (acme.jose.* is josepy.*) if mod == 'josepy' or mod.startswith('josepy.'): sys.modules['acme.' + mod.replace('josepy', 'jose', 1)] = sys.modules[mod] + +if sys.version_info[0] == 2: + warnings.warn( + "Python 2 support will be dropped in the next release of acme. " + "Please upgrade your Python version.", + PendingDeprecationWarning, + ) # pragma: no cover diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index b6283d6ccd6..0965cd60339 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -186,6 +186,7 @@ def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-argu raise errors.Error(error) return client_ssl.get_peer_certificate() + def make_csr(private_key_pem, domains, must_staple=False): """Generate a CSR containing a list of domains as subjectAltNames. @@ -226,6 +227,7 @@ def make_csr(private_key_pem, domains, must_staple=False): return crypto.dump_certificate_request( crypto.FILETYPE_PEM, csr) + def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req): common_name = loaded_cert_or_req.get_subject().CN sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req) @@ -234,6 +236,7 @@ def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req): return sans return [common_name] + [d for d in sans if d != common_name] + def _pyopenssl_cert_or_req_san(cert_or_req): #for some reason this always return nothing """Get Subject Alternative Names from certificate or CSR using pyOpenSSL. @@ -367,6 +370,7 @@ def gen_ss_cert(key, domains, not_before=None, cert.sign(key, "sha256") return cert + def dump_pyopenssl_chain(chain, filetype=crypto.FILETYPE_PEM): """Dump certificate chain into a bundle. diff --git a/acme/setup.py b/acme/setup.py index 96adc296303..f8f9efaad16 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -5,7 +5,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -66,6 +66,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], diff --git a/acme/tests/messages_test.py b/acme/tests/messages_test.py index 3458105b2ed..70b05b419c5 100644 --- a/acme/tests/messages_test.py +++ b/acme/tests/messages_test.py @@ -108,11 +108,11 @@ def test_repr(self): def test_equality(self): const_a_prime = self.MockConstant('a') - self.assertFalse(self.const_a == self.const_b) - self.assertTrue(self.const_a == const_a_prime) + self.assertNotEqual(self.const_a, self.const_b) + self.assertEqual(self.const_a, const_a_prime) - self.assertTrue(self.const_a != self.const_b) - self.assertFalse(self.const_a != const_a_prime) + self.assertNotEqual(self.const_a, self.const_b) + self.assertEqual(self.const_a, const_a_prime) class DirectoryTest(unittest.TestCase): diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py index af9bf5bf488..5c8790beb04 100644 --- a/certbot-apache/certbot_apache/_internal/configurator.py +++ b/certbot-apache/certbot_apache/_internal/configurator.py @@ -327,6 +327,9 @@ def prepare(self): if self.version < (2, 2): raise errors.NotSupportedError( "Apache Version {0} not supported.".format(str(self.version))) + elif self.version < (2, 4): + logger.warning('Support for Apache 2.2 is deprecated and will be removed in a ' + 'future release.') # Recover from previous crash before Augeas initialization to have the # correct parse tree from the get go. diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index eedc10c263e..571f4474e49 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -5,7 +5,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -53,6 +53,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-apache/tests/configurator_test.py b/certbot-apache/tests/configurator_test.py index dbb698f718e..62308cea188 100644 --- a/certbot-apache/tests/configurator_test.py +++ b/certbot-apache/tests/configurator_test.py @@ -1360,10 +1360,10 @@ def test_choose_vhosts_wildcard(self): # And the actual returned values self.assertEqual(len(vhs), 1) - self.assertTrue(vhs[0].name == "certbot.demo") + self.assertEqual(vhs[0].name, "certbot.demo") self.assertTrue(vhs[0].ssl) - self.assertFalse(vhs[0] == self.vh_truth[3]) + self.assertNotEqual(vhs[0], self.vh_truth[3]) @mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.make_vhost_ssl") def test_choose_vhosts_wildcard_no_ssl(self, mock_makessl): @@ -1474,10 +1474,10 @@ def mock_match(aug_expr): self.config.parser.aug.match = mock_match vhs = self.config.get_virtual_hosts() self.assertEqual(len(vhs), 2) - self.assertTrue(vhs[0] == self.vh_truth[1]) + self.assertEqual(vhs[0], self.vh_truth[1]) # mock_vhost should have replaced the vh_truth[0], because its filepath # isn't a symlink - self.assertTrue(vhs[1] == mock_vhost) + self.assertEqual(vhs[1], mock_vhost) class AugeasVhostsTest(util.ApacheTest): diff --git a/certbot-apache/tests/dualnode_test.py b/certbot-apache/tests/dualnode_test.py index 44cc69ff443..ef7f8b2d84f 100644 --- a/certbot-apache/tests/dualnode_test.py +++ b/certbot-apache/tests/dualnode_test.py @@ -412,9 +412,9 @@ def test_parsernode_notequal(self): ancestor=self.block, filepath="/path/to/whatever", metadata=self.metadata) - self.assertFalse(self.block == ne_block) - self.assertFalse(self.directive == ne_directive) - self.assertFalse(self.comment == ne_comment) + self.assertNotEqual(self.block, ne_block) + self.assertNotEqual(self.directive, ne_directive) + self.assertNotEqual(self.comment, ne_comment) def test_parsed_paths(self): mock_p = mock.MagicMock(return_value=['/path/file.conf', diff --git a/certbot-apache/tests/obj_test.py b/certbot-apache/tests/obj_test.py index eac6a64ef2e..8aae3ad4598 100644 --- a/certbot-apache/tests/obj_test.py +++ b/certbot-apache/tests/obj_test.py @@ -27,14 +27,14 @@ def test_repr(self): "certbot_apache._internal.obj.Addr(('127.0.0.1', '443'))") def test_eq(self): - self.assertTrue(self.vhost1b == self.vhost1) - self.assertFalse(self.vhost1 == self.vhost2) + self.assertEqual(self.vhost1b, self.vhost1) + self.assertNotEqual(self.vhost1, self.vhost2) self.assertEqual(str(self.vhost1b), str(self.vhost1)) - self.assertFalse(self.vhost1b == 1234) + self.assertNotEqual(self.vhost1b, 1234) def test_ne(self): - self.assertTrue(self.vhost1 != self.vhost2) - self.assertFalse(self.vhost1 != self.vhost1b) + self.assertNotEqual(self.vhost1, self.vhost2) + self.assertEqual(self.vhost1, self.vhost1b) def test_conflicts(self): from certbot_apache._internal.obj import Addr @@ -128,13 +128,13 @@ def test_conflicts(self): self.assertTrue(self.addr1.conflicts(self.addr2)) def test_equal(self): - self.assertTrue(self.addr1 == self.addr2) - self.assertFalse(self.addr == self.addr1) - self.assertFalse(self.addr == 123) + self.assertEqual(self.addr1, self.addr2) + self.assertNotEqual(self.addr, self.addr1) + self.assertNotEqual(self.addr, 123) def test_not_equal(self): - self.assertFalse(self.addr1 != self.addr2) - self.assertTrue(self.addr != self.addr1) + self.assertEqual(self.addr1, self.addr2) + self.assertNotEqual(self.addr, self.addr1) if __name__ == "__main__": diff --git a/certbot-auto b/certbot-auto index 2a0cda9b332..ee12d470690 100755 --- a/certbot-auto +++ b/certbot-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="1.9.0" +LE_AUTO_VERSION="1.10.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -799,11 +799,7 @@ BootstrapMageiaCommon() { # that function. If Bootstrap is set to a function that doesn't install any # packages BOOTSTRAP_VERSION is not set. if [ -f /etc/debian_version ]; then - Bootstrap() { - BootstrapMessage "Debian-based OSes" - BootstrapDebCommon - } - BOOTSTRAP_VERSION="BootstrapDebCommon $BOOTSTRAP_DEB_COMMON_VERSION" + DEPRECATED_OS=1 elif [ -f /etc/mageia-release ]; then # Mageia has both /etc/mageia-release and /etc/redhat-release DEPRECATED_OS=1 @@ -1497,18 +1493,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==1.9.0 \ - --hash=sha256:d5a804d32e471050921f7b39ed9859e2e9de02824176ed78f57266222036b53a \ - --hash=sha256:2ff9bf7d9af381c7efee22dec2dd6938d9d8fddcc9e11682b86e734164a30b57 -acme==1.9.0 \ - --hash=sha256:d8061b396a22b21782c9b23ff9a945b23e50fca2573909a42f845e11d5658ac5 \ - --hash=sha256:38a1630c98e144136c62eec4d2c545a1bdb1a3cd4eca82214be6b83a1f5a161f -certbot-apache==1.9.0 \ - --hash=sha256:09528a820d57e54984d490100644cd8a6603db97bf5776f86e95795ecfacf23d \ - --hash=sha256:f47fb3f4a9bd927f4812121a0beefe56b163475a28f4db34c64dc838688d9e9e -certbot-nginx==1.9.0 \ - --hash=sha256:bb2e3f7fe17f071f350a3efa48571b8ef40a8e4b6db9c6da72539206a20b70be \ - --hash=sha256:ab26a4f49d53b0e8bf0f903e58e2a840cda233fe1cbbc54c36ff17f973e57d65 +certbot==1.10.1 \ + --hash=sha256:011ac980fa21b9f29e02c9b8d8b86e8a4bf4670b51b6ad91656e401e9d2d2231 \ + --hash=sha256:0d9ee3fc09e0d03b2d1b1f1c4916e61ecfc6904b4216ddef4e6a5ca1424d9cb7 +acme==1.10.1 \ + --hash=sha256:752d598e54e98ad1e874de53fd50c61044f1b566d6deb790db5676ce9c573546 \ + --hash=sha256:fcbb559aedc96b404edf593e78517dcd7291984d5a37036c3fc77f3c5c122fd8 +certbot-apache==1.10.1 \ + --hash=sha256:f077b4b7f166627ef5e0921fe7cde57700670fc86e9ad9dbdfaf2c573cc0f2fa \ + --hash=sha256:97ed637b4c7b03820db6c69aa90145dc989933351d46a3d62baf6b71674f0a10 +certbot-nginx==1.10.1 \ + --hash=sha256:7c36459021f8a1ec3b6c062e4c4fc866bfaa1dbf26ccd29e043dd6848003be08 \ + --hash=sha256:c0bbeccf85f46b728fd95e6bb8c2649d32d3383d7f47ea4b9c312d12bf04d2f0 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-ci/certbot_integration_tests/assets/bind-config/conf/named.conf b/certbot-ci/certbot_integration_tests/assets/bind-config/conf/named.conf new file mode 100644 index 00000000000..672a447d337 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/bind-config/conf/named.conf @@ -0,0 +1,60 @@ +options { + directory "/var/cache/bind"; + + // Running inside Docker. Bind address on Docker host is 127.0.0.1. + listen-on { any; }; + listen-on-v6 { any; }; + + // We are allowing BIND to service recursive queries, but only in an extremely limimited sense + // where it is entirely disconnected from public DNS: + // - Iterative queries are disabled. Only forwarding to a non-existent forwarder. + // - The only recursive answers we can get (that will not be a SERVFAIL) will come from the + // RPZ "mock-recursion" zone. Effectively this means we are mocking out the entirety of + // public DNS. + allow-recursion { any; }; // BIND will only answer using RPZ if recursion is enabled + forwarders { 192.0.2.254; }; // Nobody is listening, this is TEST-NET-1 + forward only; // Do NOT perform iterative queries from the root zone + dnssec-validation no; // Do not bother fetching the root DNSKEY set (performance) + response-policy { // All recursive queries will be served from here. + zone "mock-recursion" + log yes; + } recursive-only no // Allow RPZs to affect authoritative zones too. + qname-wait-recurse no // No real recursion. + nsip-wait-recurse no; // No real recursion. + + allow-transfer { none; }; + allow-update { none; }; +}; + +key "default-key." { + algorithm hmac-sha512; + secret "91CgOwzihr0nAVEHKFXJPQCbuBBbBI19Ks5VAweUXgbF40NWTD83naeg3c5y2MPdEiFRXnRLJxL6M+AfHCGLNw=="; +}; + +zone "mock-recursion" { + type primary; + file "/var/lib/bind/rpz.mock-recursion"; + allow-query { + none; + }; +}; + +zone "example.com." { + type primary; + file "/var/lib/bind/db.example.com"; + journal "/var/cache/bind/db.example.com.jnl"; + + update-policy { + grant default-key zonesub TXT; + }; +}; + +zone "sub.example.com." { + type primary; + file "/var/lib/bind/db.sub.example.com"; + journal "/var/cache/bind/db.sub.example.com.jnl"; + + update-policy { + grant default-key zonesub TXT; + }; +}; diff --git a/certbot-ci/certbot_integration_tests/assets/bind-config/rfc2136-credentials-default.ini.tpl b/certbot-ci/certbot_integration_tests/assets/bind-config/rfc2136-credentials-default.ini.tpl new file mode 100644 index 00000000000..8aa7cc3cbdd --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/bind-config/rfc2136-credentials-default.ini.tpl @@ -0,0 +1,10 @@ +# Target DNS server +dns_rfc2136_server = {server_address} +# Target DNS port +dns_rfc2136_port = {server_port} +# TSIG key name +dns_rfc2136_name = default-key. +# TSIG key secret +dns_rfc2136_secret = 91CgOwzihr0nAVEHKFXJPQCbuBBbBI19Ks5VAweUXgbF40NWTD83naeg3c5y2MPdEiFRXnRLJxL6M+AfHCGLNw== +# TSIG key algorithm +dns_rfc2136_algorithm = HMAC-SHA512 diff --git a/certbot-ci/certbot_integration_tests/assets/bind-config/zones/db.example.com b/certbot-ci/certbot_integration_tests/assets/bind-config/zones/db.example.com new file mode 100644 index 00000000000..2573470b56e --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/bind-config/zones/db.example.com @@ -0,0 +1,11 @@ +$ORIGIN example.com. +$TTL 3600 +example.com. IN SOA ns1.example.com. admin.example.com. ( 2020091025 7200 3600 1209600 3600 ) + +example.com. IN NS ns1 +example.com. IN NS ns2 + +ns1 IN A 192.0.2.2 +ns2 IN A 192.0.2.3 + +@ IN A 192.0.2.1 diff --git a/certbot-ci/certbot_integration_tests/assets/bind-config/zones/db.sub.example.com b/certbot-ci/certbot_integration_tests/assets/bind-config/zones/db.sub.example.com new file mode 100644 index 00000000000..0379003b79e --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/bind-config/zones/db.sub.example.com @@ -0,0 +1,9 @@ +$ORIGIN sub.example.com. +$TTL 3600 +sub.example.com. IN SOA ns1.example.com. admin.example.com. ( 2020091025 7200 3600 1209600 3600 ) + +sub.example.com. IN NS ns1 +sub.example.com. IN NS ns2 + +ns1 IN A 192.0.2.2 +ns2 IN A 192.0.2.3 diff --git a/certbot-ci/certbot_integration_tests/assets/bind-config/zones/rpz.mock-recursion b/certbot-ci/certbot_integration_tests/assets/bind-config/zones/rpz.mock-recursion new file mode 100644 index 00000000000..589689d37da --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/bind-config/zones/rpz.mock-recursion @@ -0,0 +1,6 @@ +$TTL 3600 + +@ SOA ns1.example.test. dummy.example.test. 1 12h 15m 3w 2h + NS ns1.example.test. + +_acme-challenge.aliased.example IN CNAME _acme-challenge.example.com. diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/README b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/README new file mode 100644 index 00000000000..5050078ff85 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/README @@ -0,0 +1,14 @@ +This directory contains your keys and certificates. + +`privkey.pem` : the private key for your certificate. +`fullchain.pem`: the certificate file used in most server software. +`chain.pem` : used for OCSP stapling in Nginx >=1.3.7. +`cert.pem` : will break many server configurations, and should not be used + without reading further documentation (see link below). + +WARNING: DO NOT MOVE OR RENAME THESE FILES! + Certbot expects these files to remain in this location in order + to function properly! + +We recommend not moving these files. For more information, see the Certbot +User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates. diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/cert.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/cert.pem new file mode 100644 index 00000000000..b07af2bf75c --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC2zCCAcOgAwIBAgIIBvrEnbPRYu8wDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE +AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjEwNzQw +WhcNMjUxMDEyMjEwNzQwWjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs +ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARjMhuW0ENPPC33PjB5XsYU +CRw640kPQENIDatcTJaENZIZdqKd6rI6jc+lpbmXot7Zi52clJlSJS+V6oDAt2Lh +o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUj7Kd3ENqxlPf8B2bIGhsjydX +mPswHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE +JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww +GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQCl +k0JXsa8y7fg41WWMDhw60bPW77O0FtOmTcnhdI5daYNemQVk+Q5EMaBLQ/oGjgXd +9QXFzXH1PL904YEnSLt+iTpXn++7rQSNzQsdYqw0neWk4f5pEBiN+WORpb6mwobV +ifMtBOkNEHvrJ2Pkci9U1lLwtKD/DSew6QtJU5DSkmH1XdGuMJiubygEIvELtvgq +cP9S368ZvPmPGmKaJQXBiuaR8MTjY/Bkr79aXQMjKbf+mpn7h0POCcePk1DY/rm6 +Da+X16lf0hHyQhSUa7Vgyim6rK1/hlw+Z00i+sQCKD9Ih7kXuuGqfSDC33cfO8Tj +o/MXO8lcxkrem5zU5QWP +-----END CERTIFICATE----- diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/chain.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/chain.pem new file mode 100644 index 00000000000..64a805c0fd1 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/chain.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx +MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy +NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq +mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB +qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5 +CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH +nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY +MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx +PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE +bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT +MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq +uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P +fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV +EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW +fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG +9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE +-----END CERTIFICATE----- diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/fullchain.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/fullchain.pem new file mode 100644 index 00000000000..1ba80ba4e9e --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/fullchain.pem @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIC2zCCAcOgAwIBAgIILlmGtZhUFEwwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE +AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjA1MDM0 +WhcNMjUxMDEyMjA1MDM0WjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs +ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARHEzR8JPWrEmpmgM+F2bk5 +9mT0u6CjzmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/ +o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU1CsVL+bPnzaxxQ5jUENmQJIO +lKwwHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE +JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww +GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBn +2D8loC7pfk28JYpFLr5lmFKJWWmtLGlpsWDj61fVjtTfGKLziJz+MM6il4Y3hIz5 +58qiFK0ue0M63dIBJ33N+XxSEXon4Q0gy/zRWfH9jtPJ3FwfjkU/RT9PAUClYi0G +ptNWnTmgQkNzousbcAtRNXuuShH3856vhUnwkX+xM+cbIDi1JVmFjcGrEEQJ0rUF +mv2ZTyfbWbUs3v4rReETi2NVzr1Ql6J+ByNcMvHODzFy3t0L6yelAw2ca1I+c9HU ++Z0tnp/ykR7eXNuVLivok8UBf5OC413lh8ZO5g+Bgzh/LdtkUuavg1MYtEX0H6mX +9U7y3nVI8WEbPGf+HDeu +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx +MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy +NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq +mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB +qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5 +CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH +nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY +MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx +PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE +bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT +MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq +uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P +fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV +EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW +fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG +9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE +-----END CERTIFICATE----- diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/privkey.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/privkey.pem new file mode 100644 index 00000000000..6843b83d6e4 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/privkey.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNgefv2dad4U1VYEi +0WkdHuqywi5QXAe30OwNTTGjhbihRANCAARHEzR8JPWrEmpmgM+F2bk59mT0u6Cj +zmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/ +-----END PRIVATE KEY----- diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/README b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/README new file mode 100644 index 00000000000..5050078ff85 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/README @@ -0,0 +1,14 @@ +This directory contains your keys and certificates. + +`privkey.pem` : the private key for your certificate. +`fullchain.pem`: the certificate file used in most server software. +`chain.pem` : used for OCSP stapling in Nginx >=1.3.7. +`cert.pem` : will break many server configurations, and should not be used + without reading further documentation (see link below). + +WARNING: DO NOT MOVE OR RENAME THESE FILES! + Certbot expects these files to remain in this location in order + to function properly! + +We recommend not moving these files. For more information, see the Certbot +User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates. diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/cert.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/cert.pem new file mode 120000 index 00000000000..23e9f36efbb --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/cert.pem @@ -0,0 +1 @@ +../../archive/c.encryption-example.com/cert.pem \ No newline at end of file diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/chain.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/chain.pem new file mode 120000 index 00000000000..3ce63c2200e --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/chain.pem @@ -0,0 +1 @@ +../../archive/c.encryption-example.com/chain.pem \ No newline at end of file diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/fullchain.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/fullchain.pem new file mode 120000 index 00000000000..5f86022af9b --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/fullchain.pem @@ -0,0 +1 @@ +../../archive/c.encryption-example.com/fullchain.pem \ No newline at end of file diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/privkey.pem b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/privkey.pem new file mode 120000 index 00000000000..4a8866fdc8b --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/privkey.pem @@ -0,0 +1 @@ +../../archive/c.encryption-example.com/privkey.pem \ No newline at end of file diff --git a/certbot-ci/certbot_integration_tests/assets/sample-config/renewal/c.encryption-example.com.conf b/certbot-ci/certbot_integration_tests/assets/sample-config/renewal/c.encryption-example.com.conf new file mode 100644 index 00000000000..0c65891e461 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/sample-config/renewal/c.encryption-example.com.conf @@ -0,0 +1,17 @@ +# renew_before_expiry = 30 days +version = 1.10.0.dev0 +archive_dir = sample-config/archive/c.encryption-example.com +cert = sample-config/live/c.encryption-example.com/cert.pem +privkey = sample-config/live/c.encryption-example.com/privkey.pem +chain = sample-config/live/c.encryption-example.com/chain.pem +fullchain = sample-config/live/c.encryption-example.com/fullchain.pem + +# Options used in the renewal process +[renewalparams] +authenticator = apache +installer = apache +account = 48d6b9e8d767eccf7e4d877d6ffa81e3 +key_type = ecdsa +config_dir = sample-config-ec +elliptic_curve = secp256r1 +manual_public_ip_logging_ok = True diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/__init__.py b/certbot-ci/certbot_integration_tests/certbot_tests/__init__.py index 60c2fcdd827..819cb3e7823 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/__init__.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/__init__.py @@ -1,3 +1,4 @@ +# pylint: disable=missing-module-docstring import pytest # Custom assertions defined in the following package need to be registered to be properly diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py index 53df6b8907b..c223d524cc2 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py @@ -2,6 +2,11 @@ import io import os +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.serialization import load_pem_private_key + try: import grp POSIX_MODE = True @@ -16,6 +21,33 @@ ADMINS_SID = 'S-1-5-32-544' +def assert_elliptic_key(key, curve): + """ + Asserts that the key at the given path is an EC key using the given curve. + :param key: path to key + :param curve: name of the expected elliptic curve + """ + with open(key, 'rb') as file: + privkey1 = file.read() + + key = load_pem_private_key(data=privkey1, password=None, backend=default_backend()) + + assert isinstance(key, EllipticCurvePrivateKey) + assert isinstance(key.curve, curve) + + +def assert_rsa_key(key): + """ + Asserts that the key at the given path is an RSA key. + :param key: path to key + """ + with open(key, 'rb') as file: + privkey1 = file.read() + + key = load_pem_private_key(data=privkey1, password=None, backend=default_backend()) + assert isinstance(key, RSAPrivateKey) + + def assert_hook_execution(probe_path, probe_content): """ Assert that a certbot hook has been executed diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/context.py b/certbot-ci/certbot_integration_tests/certbot_tests/context.py index e295aefd732..b9854b40293 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/context.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/context.py @@ -77,6 +77,6 @@ def get_domain(self, subdomain='le'): appending the pytest worker id to the subdomain, using this pattern: {subdomain}.{worker_id}.wtf :param subdomain: the subdomain to use in the generated domain (default 'le') - :return: the well-formed domain suitable for redirection on + :return: the well-formed domain suitable for redirection on """ return '{0}.{1}.wtf'.format(subdomain, self.worker_id) diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py index cfafce73240..b7b50425e7e 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py @@ -9,12 +9,15 @@ import subprocess import time +from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1, SECP384R1 from cryptography.x509 import NameOID import pytest from certbot_integration_tests.certbot_tests import context as certbot_context from certbot_integration_tests.certbot_tests.assertions import assert_cert_count_for_lineage +from certbot_integration_tests.certbot_tests.assertions import assert_elliptic_key +from certbot_integration_tests.certbot_tests.assertions import assert_rsa_key from certbot_integration_tests.certbot_tests.assertions import assert_equals_group_owner from certbot_integration_tests.certbot_tests.assertions import assert_equals_group_permissions from certbot_integration_tests.certbot_tests.assertions import assert_equals_world_read_permissions @@ -26,8 +29,9 @@ from certbot_integration_tests.utils import misc -@pytest.fixture() -def context(request): +@pytest.fixture(name='context') +def test_context(request): + # pylint: disable=missing-function-docstring # Fixture request is a built-in pytest fixture describing current test request. integration_test_context = certbot_context.IntegrationTestsContext(request) try: @@ -219,14 +223,16 @@ def test_renew_files_propagate_permissions(context): if os.name != 'nt': os.chmod(privkey1, 0o444) else: - import win32security - import ntsecuritycon + import win32security # pylint: disable=import-error + import ntsecuritycon # pylint: disable=import-error # Get the current DACL of the private key security = win32security.GetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION) dacl = security.GetSecurityDescriptorDacl() # Create a read permission for Everybody group everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) - dacl.AddAccessAllowedAce(win32security.ACL_REVISION, ntsecuritycon.FILE_GENERIC_READ, everybody) + dacl.AddAccessAllowedAce( + win32security.ACL_REVISION, ntsecuritycon.FILE_GENERIC_READ, everybody + ) # Apply the updated DACL to the private key security.SetSecurityDescriptorDacl(1, dacl, 0) win32security.SetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION, security) @@ -235,12 +241,14 @@ def test_renew_files_propagate_permissions(context): assert_cert_count_for_lineage(context.config_dir, certname, 2) if os.name != 'nt': - # On Linux, read world permissions + all group permissions will be copied from the previous private key + # On Linux, read world permissions + all group permissions + # will be copied from the previous private key assert_world_read_permissions(privkey2) assert_equals_world_read_permissions(privkey1, privkey2) assert_equals_group_permissions(privkey1, privkey2) else: - # On Windows, world will never have any permissions, and group permission is irrelevant for this platform + # On Windows, world will never have any permissions, and + # group permission is irrelevant for this platform assert_world_no_permissions(privkey2) @@ -289,7 +297,7 @@ def test_renew_with_changed_private_key_complexity(context): assert_cert_count_for_lineage(context.config_dir, certname, 1) context.certbot(['renew']) - + assert_cert_count_for_lineage(context.config_dir, certname, 2) key2 = join(context.config_dir, 'archive', certname, 'privkey2.pem') assert os.stat(key2).st_size > 3000 @@ -421,20 +429,93 @@ def test_reuse_key(context): assert len({cert1, cert2, cert3}) == 3 +def test_incorrect_key_type(context): + with pytest.raises(subprocess.CalledProcessError): + context.certbot(['--key-type="failwhale"']) + + def test_ecdsa(context): - """Test certificate issuance with ECDSA key.""" + """Test issuance for ECDSA CSR based request (legacy supported mode).""" key_path = join(context.workspace, 'privkey-p384.pem') csr_path = join(context.workspace, 'csr-p384.der') cert_path = join(context.workspace, 'cert-p384.pem') chain_path = join(context.workspace, 'chain-p384.pem') - misc.generate_csr([context.get_domain('ecdsa')], key_path, csr_path, key_type=misc.ECDSA_KEY_TYPE) - context.certbot(['auth', '--csr', csr_path, '--cert-path', cert_path, '--chain-path', chain_path]) + misc.generate_csr( + [context.get_domain('ecdsa')], + key_path, csr_path, + key_type=misc.ECDSA_KEY_TYPE + ) + context.certbot([ + 'auth', '--csr', csr_path, '--cert-path', cert_path, + '--chain-path', chain_path, + ]) certificate = misc.read_certificate(cert_path) assert 'ASN1 OID: secp384r1' in certificate +def test_default_key_type(context): + """Test default key type is RSA""" + certname = context.get_domain('renew') + context.certbot([ + 'certonly', + '--cert-name', certname, '-d', certname + ]) + filename = join(context.config_dir, 'archive/{0}/privkey1.pem').format(certname) + assert_rsa_key(filename) + + +def test_default_curve_type(context): + """test that the curve used when not specifying any is secp256r1""" + certname = context.get_domain('renew') + context.certbot([ + '--key-type', 'ecdsa', '--cert-name', certname, '-d', certname + ]) + key1 = join(context.config_dir, 'archive/{0}/privkey1.pem'.format(certname)) + assert_elliptic_key(key1, SECP256R1) + + +def test_renew_with_ec_keys(context): + """Test proper renew with updated private key complexity.""" + certname = context.get_domain('renew') + context.certbot([ + 'certonly', + '--cert-name', certname, + '--key-type', 'ecdsa', '--elliptic-curve', 'secp256r1', + '--force-renewal', '-d', certname, + ]) + + key1 = join(context.config_dir, "archive", certname, 'privkey1.pem') + assert 200 < os.stat(key1).st_size < 250 # ec keys of 256 bits are ~225 bytes + assert_elliptic_key(key1, SECP256R1) + assert_cert_count_for_lineage(context.config_dir, certname, 1) + + context.certbot(['renew', '--elliptic-curve', 'secp384r1']) + + assert_cert_count_for_lineage(context.config_dir, certname, 2) + key2 = join(context.config_dir, 'archive', certname, 'privkey2.pem') + assert_elliptic_key(key2, SECP384R1) + assert 280 < os.stat(key2).st_size < 320 # ec keys of 384 bits are ~310 bytes + + # We expect here that the command will fail because without --key-type specified, + # Certbot must error out to prevent changing an existing certificate key type, + # without explicit user consent (by specifying both --cert-name and --key-type). + with pytest.raises(subprocess.CalledProcessError): + context.certbot([ + 'certonly', + '--force-renewal', + '-d', certname + ]) + + # We expect that the previous behavior of requiring both --cert-name and + # --key-type to be set to not apply to the renew subcommand. + context.certbot(['renew', '--force-renewal', '--key-type', 'rsa']) + assert_cert_count_for_lineage(context.config_dir, certname, 3) + key3 = join(context.config_dir, 'archive', certname, 'privkey3.pem') + assert_rsa_key(key3) + + def test_ocsp_must_staple(context): """Test that OCSP Must-Staple is correctly set in the generated certificate.""" if context.acme_server == 'pebble': @@ -533,14 +614,17 @@ def test_revoke_multiple_lineages(context): with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'r') as file: data = file.read() - data = re.sub('archive_dir = .*\n', - 'archive_dir = {0}\n'.format(join(context.config_dir, 'archive', cert1).replace('\\', '\\\\')), - data) + data = re.sub( + 'archive_dir = .*\n', + 'archive_dir = {0}\n'.format( + join(context.config_dir, 'archive', cert1).replace('\\', '\\\\') + ), data + ) with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'w') as file: file.write(data) - output = context.certbot([ + context.certbot([ 'revoke', '--cert-path', join(context.config_dir, 'live', cert1, 'cert.pem') ]) @@ -658,4 +742,4 @@ def test_preferred_chain(context): with open(conf_path, 'r') as f: assert 'preferred_chain = {}'.format(requested) in f.read(), \ - 'Expected preferred_chain to be set in renewal config' \ No newline at end of file + 'Expected preferred_chain to be set in renewal config' diff --git a/certbot-ci/certbot_integration_tests/conftest.py b/certbot-ci/certbot_integration_tests/conftest.py index bb1d76e575f..230fb0eda64 100644 --- a/certbot-ci/certbot_integration_tests/conftest.py +++ b/certbot-ci/certbot_integration_tests/conftest.py @@ -12,6 +12,7 @@ import sys from certbot_integration_tests.utils import acme_server as acme_lib +from certbot_integration_tests.utils import dns_server as dns_lib def pytest_addoption(parser): @@ -23,6 +24,10 @@ def pytest_addoption(parser): choices=['boulder-v1', 'boulder-v2', 'pebble'], help='select the ACME server to use (boulder-v1, boulder-v2, ' 'pebble), defaulting to pebble') + parser.addoption('--dns-server', default='challtestsrv', + choices=['bind', 'challtestsrv'], + help='select the DNS server to use (bind, challtestsrv), ' + 'defaulting to challtestsrv') def pytest_configure(config): @@ -32,7 +37,7 @@ def pytest_configure(config): """ if not hasattr(config, 'slaveinput'): # If true, this is the primary node with _print_on_err(): - config.acme_xdist = _setup_primary_node(config) + _setup_primary_node(config) def pytest_configure_node(node): @@ -41,6 +46,7 @@ def pytest_configure_node(node): :param node: current worker node """ node.slaveinput['acme_xdist'] = node.config.acme_xdist + node.slaveinput['dns_xdist'] = node.config.dns_xdist @contextlib.contextmanager @@ -61,12 +67,18 @@ def _print_on_err(): def _setup_primary_node(config): """ Setup the environment for integration tests. - Will: + + This function will: - check runtime compatibility (Docker, docker-compose, Nginx) - create a temporary workspace and the persistent GIT repositories space + - configure and start a DNS server using Docker, if configured - configure and start paralleled ACME CA servers using Docker - - transfer ACME CA servers configurations to pytest nodes using env variables - :param config: Configuration of the pytest primary node + - transfer ACME CA and DNS servers configurations to pytest nodes using env variables + + This function modifies `config` by injecting the ACME CA and DNS server configurations, + in addition to cleanup functions for those servers. + + :param config: Configuration of the pytest primary node. Is modified by this function. """ # Check for runtime compatibility: some tools are required to be available in PATH if 'boulder' in config.option.acme_server: @@ -79,18 +91,35 @@ def _setup_primary_node(config): try: subprocess.check_output(['docker-compose', '-v'], stderr=subprocess.STDOUT) except (subprocess.CalledProcessError, OSError): - raise ValueError('Error: docker-compose is required in PATH to launch the integration tests, ' - 'but is not installed or not available for current user.') + raise ValueError( + 'Error: docker-compose is required in PATH to launch the integration tests, ' + 'but is not installed or not available for current user.' + ) # Parameter numprocesses is added to option by pytest-xdist workers = ['primary'] if not config.option.numprocesses\ else ['gw{0}'.format(i) for i in range(config.option.numprocesses)] + # If a non-default DNS server is configured, start it and feed it to the ACME server + dns_server = None + acme_dns_server = None + if config.option.dns_server == 'bind': + dns_server = dns_lib.DNSServer(workers) + config.add_cleanup(dns_server.stop) + print('DNS xdist config:\n{0}'.format(dns_server.dns_xdist)) + dns_server.start() + acme_dns_server = '{}:{}'.format( + dns_server.dns_xdist['address'], + dns_server.dns_xdist['port'] + ) + # By calling setup_acme_server we ensure that all necessary acme server instances will be # fully started. This runtime is reflected by the acme_xdist returned. - acme_server = acme_lib.ACMEServer(config.option.acme_server, workers) + acme_server = acme_lib.ACMEServer(config.option.acme_server, workers, + dns_server=acme_dns_server) config.add_cleanup(acme_server.stop) print('ACME xdist config:\n{0}'.format(acme_server.acme_xdist)) acme_server.start() - return acme_server.acme_xdist + config.acme_xdist = acme_server.acme_xdist + config.dns_xdist = dns_server.dns_xdist if dns_server else None diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/context.py b/certbot-ci/certbot_integration_tests/nginx_tests/context.py index 3a769840caa..6f0f833a091 100644 --- a/certbot-ci/certbot_integration_tests/nginx_tests/context.py +++ b/certbot-ci/certbot_integration_tests/nginx_tests/context.py @@ -1,3 +1,4 @@ +"""Module to handle the context of nginx integration tests.""" import os import subprocess diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py b/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py index 18991ae6217..bbbe8ea06fc 100644 --- a/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py +++ b/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """General purpose nginx test configuration generator.""" import getpass @@ -42,6 +43,8 @@ def construct_nginx_config(nginx_root, nginx_webroot, http_port, https_port, oth worker_connections 1024; }} +# “This comment contains valid Unicode”. + http {{ # Set an array of temp, cache and log file options that will otherwise default to # restricted locations accessible only to root. @@ -51,61 +54,61 @@ def construct_nginx_config(nginx_root, nginx_webroot, http_port, https_port, oth #scgi_temp_path {nginx_root}/scgi_temp; #uwsgi_temp_path {nginx_root}/uwsgi_temp; access_log {nginx_root}/error.log; - + # This should be turned off in a Virtualbox VM, as it can cause some # interesting issues with data corruption in delivered files. sendfile off; - + tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; - + #include /etc/nginx/mime.types; index index.html index.htm index.php; - + log_format main '$remote_addr - $remote_user [$time_local] $status ' '"$request" $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - + default_type application/octet-stream; - + server {{ # IPv4. listen {http_port} {default_server}; # IPv6. listen [::]:{http_port} {default_server}; server_name nginx.{wtf_prefix}.wtf nginx2.{wtf_prefix}.wtf; - + root {nginx_webroot}; - + location / {{ # First attempt to serve request as file, then as directory, then fall # back to index.html. try_files $uri $uri/ /index.html; }} }} - + server {{ listen {http_port}; listen [::]:{http_port}; server_name nginx3.{wtf_prefix}.wtf; - + root {nginx_webroot}; - + location /.well-known/ {{ return 404; }} - + return 301 https://$host$request_uri; }} - + server {{ listen {other_port}; listen [::]:{other_port}; server_name nginx4.{wtf_prefix}.wtf nginx5.{wtf_prefix}.wtf; }} - + server {{ listen {http_port}; listen [::]:{http_port}; diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py b/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py index 1a62ea8d7b5..e6e66126e0f 100644 --- a/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py +++ b/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py @@ -7,8 +7,8 @@ from certbot_integration_tests.nginx_tests import context as nginx_context -@pytest.fixture() -def context(request): +@pytest.fixture(name='context') +def test_context(request): # Fixture request is a built-in pytest fixture describing current test request. integration_test_context = nginx_context.IntegrationTestsContext(request) try: @@ -27,7 +27,9 @@ def context(request): # No matching server block; default_server does not exist ('nginx5.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': False}), # Multiple domains, mix of matching and not - ('nginx6.{0}.wtf,nginx7.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': False}), + ('nginx6.{0}.wtf,nginx7.{0}.wtf', [ + '--preferred-challenges', 'http' + ], {'default_server': False}), ], indirect=['context']) def test_certificate_deployment(certname_pattern, params, context): # type: (str, list, nginx_context.IntegrationTestsContext) -> None @@ -41,7 +43,9 @@ def test_certificate_deployment(certname_pattern, params, context): lineage = domains.split(',')[0] server_cert = ssl.get_server_certificate(('localhost', context.tls_alpn_01_port)) - with open(os.path.join(context.workspace, 'conf/live/{0}/cert.pem'.format(lineage)), 'r') as file: + with open(os.path.join( + context.workspace, 'conf/live/{0}/cert.pem'.format(lineage)), 'r' + ) as file: certbot_cert = file.read() assert server_cert == certbot_cert diff --git a/certbot-ci/certbot_integration_tests/rfc2136_tests/__init__.py b/certbot-ci/certbot_integration_tests/rfc2136_tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py b/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py new file mode 100644 index 00000000000..bdedee1fee7 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py @@ -0,0 +1,66 @@ +"""Module to handle the context of RFC2136 integration tests.""" + +import tempfile +from contextlib import contextmanager + +from pkg_resources import resource_filename +from pytest import skip + +from certbot_integration_tests.certbot_tests import context as certbot_context +from certbot_integration_tests.utils import certbot_call + + +class IntegrationTestsContext(certbot_context.IntegrationTestsContext): + """Integration test context for certbot-dns-rfc2136""" + def __init__(self, request): + super(IntegrationTestsContext, self).__init__(request) + + self.request = request + + self._dns_xdist = None + if hasattr(request.config, 'slaveinput'): # Worker node + self._dns_xdist = request.config.slaveinput['dns_xdist'] + else: # Primary node + self._dns_xdist = request.config.dns_xdist + + def certbot_test_rfc2136(self, args): + """ + Main command to execute certbot using the RFC2136 DNS authenticator. + :param list args: list of arguments to pass to Certbot + """ + command = ['--authenticator', 'dns-rfc2136', '--dns-rfc2136-propagation-seconds', '2'] + command.extend(args) + return certbot_call.certbot_test( + command, self.directory_url, self.http_01_port, self.tls_alpn_01_port, + self.config_dir, self.workspace, force_renew=True) + + @contextmanager + def rfc2136_credentials(self, label='default'): + """ + Produces the contents of a certbot-dns-rfc2136 credentials file. + :param str label: which RFC2136 credential to use + :yields: Path to credentials file + :rtype: str + """ + src_file = resource_filename('certbot_integration_tests', + 'assets/bind-config/rfc2136-credentials-{}.ini.tpl' + .format(label)) + contents = None + + with open(src_file, 'r') as f: + contents = f.read().format( + server_address=self._dns_xdist['address'], + server_port=self._dns_xdist['port'] + ) + + with tempfile.NamedTemporaryFile('w+', prefix='rfc2136-creds-{}'.format(label), + suffix='.ini', dir=self.workspace) as fp: + fp.write(contents) + fp.flush() + yield fp.name + + def skip_if_no_bind9_server(self): + """Skips the test if there was no RFC2136-capable DNS server configured + in the test environment""" + if not self._dns_xdist: + skip('No RFC2136-capable DNS server is configured') diff --git a/certbot-ci/certbot_integration_tests/rfc2136_tests/test_main.py b/certbot-ci/certbot_integration_tests/rfc2136_tests/test_main.py new file mode 100644 index 00000000000..ae6c0018ec3 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/rfc2136_tests/test_main.py @@ -0,0 +1,26 @@ +"""Module executing integration tests against Certbot with the RFC2136 DNS authenticator.""" +import pytest + +from certbot_integration_tests.rfc2136_tests import context as rfc2136_context + + +@pytest.fixture(name="context") +def pytest_context(request): + # pylint: disable=missing-function-docstring + # Fixture request is a built-in pytest fixture describing current test request. + integration_test_context = rfc2136_context.IntegrationTestsContext(request) + try: + yield integration_test_context + finally: + integration_test_context.cleanup() + + +@pytest.mark.parametrize('domain', [('example.com'), ('sub.example.com')]) +def test_get_certificate(domain, context): + context.skip_if_no_bind9_server() + + with context.rfc2136_credentials() as creds: + context.certbot_test_rfc2136([ + 'certonly', '--dns-rfc2136-credentials', creds, + '-d', domain, '-d', '*.{}'.format(domain) + ]) diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py index c73b6c895a4..a730e51876c 100755 --- a/certbot-ci/certbot_integration_tests/utils/acme_server.py +++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py @@ -2,6 +2,7 @@ """Module to setup an ACME CA server environment able to run multiple tests in parallel""" from __future__ import print_function +import argparse import errno import json import os @@ -14,9 +15,11 @@ import requests +from acme.magic_typing import List from certbot_integration_tests.utils import misc from certbot_integration_tests.utils import pebble_artifacts from certbot_integration_tests.utils import proxy +# pylint: disable=wildcard-import,unused-wildcard-import from certbot_integration_tests.utils.constants import * @@ -29,24 +32,26 @@ class ACMEServer(object): ACMEServer gives access the acme_xdist parameter, listing the ports and directory url to use for each pytest node. It exposes also start and stop methods in order to start the stack, and stop it with proper resources cleanup. - ACMEServer is also a context manager, and so can be used to ensure ACME server is started/stopped - upon context enter/exit. + ACMEServer is also a context manager, and so can be used to ensure ACME server is + started/stopped upon context enter/exit. """ - def __init__(self, acme_server, nodes, http_proxy=True, stdout=False): + def __init__(self, acme_server, nodes, http_proxy=True, stdout=False, dns_server=None): """ Create an ACMEServer instance. :param str acme_server: the type of acme server used (boulder-v1, boulder-v2 or pebble) :param list nodes: list of node names that will be setup by pytest xdist :param bool http_proxy: if False do not start the HTTP proxy :param bool stdout: if True stream all subprocesses stdout to standard stdout + :param str dns_server: if set, Pebble/Boulder will use it to resolve domains """ self._construct_acme_xdist(acme_server, nodes) self._acme_type = 'pebble' if acme_server == 'pebble' else 'boulder' self._proxy = http_proxy self._workspace = tempfile.mkdtemp() - self._processes = [] + self._processes = [] # type: List self._stdout = sys.stdout if stdout else open(os.devnull, 'w') + self._dns_server = dns_server def start(self): """Start the test stack""" @@ -103,19 +108,26 @@ def _construct_acme_xdist(self, acme_server, nodes): """Generate and return the acme_xdist dict""" acme_xdist = {'acme_server': acme_server, 'challtestsrv_port': CHALLTESTSRV_PORT} - # Directory and ACME port are set implicitly in the docker-compose.yml files of Boulder/Pebble. + # Directory and ACME port are set implicitly in the docker-compose.yml + # files of Boulder/Pebble. if acme_server == 'pebble': acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL else: # boulder acme_xdist['directory_url'] = BOULDER_V2_DIRECTORY_URL \ if acme_server == 'boulder-v2' else BOULDER_V1_DIRECTORY_URL - acme_xdist['http_port'] = {node: port for (node, port) - in zip(nodes, range(5200, 5200 + len(nodes)))} - acme_xdist['https_port'] = {node: port for (node, port) - in zip(nodes, range(5100, 5100 + len(nodes)))} - acme_xdist['other_port'] = {node: port for (node, port) - in zip(nodes, range(5300, 5300 + len(nodes)))} + acme_xdist['http_port'] = { + node: port for (node, port) in # pylint: disable=unnecessary-comprehension + zip(nodes, range(5200, 5200 + len(nodes))) + } + acme_xdist['https_port'] = { + node: port for (node, port) in # pylint: disable=unnecessary-comprehension + zip(nodes, range(5100, 5100 + len(nodes))) + } + acme_xdist['other_port'] = { + node: port for (node, port) in # pylint: disable=unnecessary-comprehension + zip(nodes, range(5300, 5300 + len(nodes))) + } self.acme_xdist = acme_xdist @@ -132,18 +144,23 @@ def _prepare_pebble_server(self): environ['PEBBLE_AUTHZREUSE'] = '100' environ['PEBBLE_ALTERNATE_ROOTS'] = str(PEBBLE_ALTERNATE_ROOTS) - self._launch_process( - [pebble_path, '-config', pebble_config_path, '-dnsserver', '127.0.0.1:8053', '-strict'], - env=environ) + if self._dns_server: + dns_server = self._dns_server + else: + dns_server = '127.0.0.1:8053' + self._launch_process( + [challtestsrv_path, '-management', ':{0}'.format(CHALLTESTSRV_PORT), + '-defaultIPv6', '""', '-defaultIPv4', '127.0.0.1', '-http01', '""', + '-tlsalpn01', '""', '-https01', '""']) self._launch_process( - [challtestsrv_path, '-management', ':{0}'.format(CHALLTESTSRV_PORT), '-defaultIPv6', '""', - '-defaultIPv4', '127.0.0.1', '-http01', '""', '-tlsalpn01', '""', '-https01', '""']) + [pebble_path, '-config', pebble_config_path, '-dnsserver', dns_server, '-strict'], + env=environ) - # pebble_ocsp_server is imported here and not at the top of module in order to avoid a useless - # ImportError, in the case where cryptography dependency is too old to support ocsp, but - # Boulder is used instead of Pebble, so pebble_ocsp_server is not used. This is the typical - # situation of integration-certbot-oldest tox testenv. + # pebble_ocsp_server is imported here and not at the top of module in order to avoid a + # useless ImportError, in the case where cryptography dependency is too old to support + # ocsp, but Boulder is used instead of Pebble, so pebble_ocsp_server is not used. This is + # the typical situation of integration-certbot-oldest tox testenv. from certbot_integration_tests.utils import pebble_ocsp_server self._launch_process([sys.executable, pebble_ocsp_server.__file__]) @@ -167,6 +184,15 @@ def _prepare_boulder_server(self): os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'), join(instance_path, 'test/rate-limit-policies.yml')) + if self._dns_server: + # Change Boulder config to use the provided DNS server + for suffix in ["", "-remote-a", "-remote-b"]: + with open(join(instance_path, 'test/config/va{}.json'.format(suffix)), 'r') as f: + config = json.loads(f.read()) + config['va']['dnsResolvers'] = [self._dns_server] + with open(join(instance_path, 'test/config/va{}.json'.format(suffix)), 'w') as f: + f.write(json.dumps(config, indent=2, separators=(',', ': '))) + try: # Launch the Boulder server self._launch_process(['docker-compose', 'up', '--force-recreate'], cwd=instance_path) @@ -175,14 +201,18 @@ def _prepare_boulder_server(self): print('=> Waiting for boulder instance to respond...') misc.check_until_timeout(self.acme_xdist['directory_url'], attempts=300) - # Configure challtestsrv to answer any A record request with ip of the docker host. - response = requests.post('http://localhost:{0}/set-default-ipv4'.format(CHALLTESTSRV_PORT), - json={'ip': '10.77.77.1'}) - response.raise_for_status() + if not self._dns_server: + # Configure challtestsrv to answer any A record request with ip of the docker host. + response = requests.post('http://localhost:{0}/set-default-ipv4'.format( + CHALLTESTSRV_PORT), json={'ip': '10.77.77.1'} + ) + response.raise_for_status() except BaseException: # If we failed to set up boulder, print its logs. print('=> Boulder setup failed. Boulder logs are:') - process = self._launch_process(['docker-compose', 'logs'], cwd=instance_path, force_stderr=True) + process = self._launch_process([ + 'docker-compose', 'logs'], cwd=instance_path, force_stderr=True + ) process.wait() raise @@ -202,20 +232,31 @@ def _launch_process(self, command, cwd=os.getcwd(), env=None, force_stderr=False if not env: env = os.environ stdout = sys.stderr if force_stderr else self._stdout - process = subprocess.Popen(command, stdout=stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env) + process = subprocess.Popen( + command, stdout=stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env + ) self._processes.append(process) return process def main(): - args = sys.argv[1:] - server_type = args[0] if args else 'pebble' - possible_values = ('pebble', 'boulder-v1', 'boulder-v2') - if server_type not in possible_values: - raise ValueError('Invalid server value {0}, should be one of {1}' - .format(server_type, possible_values)) - - acme_server = ACMEServer(server_type, [], http_proxy=False, stdout=True) + # pylint: disable=missing-function-docstring + parser = argparse.ArgumentParser( + description='CLI tool to start a local instance of Pebble or Boulder CA server.') + parser.add_argument('--server-type', '-s', + choices=['pebble', 'boulder-v1', 'boulder-v2'], default='pebble', + help='type of CA server to start: can be Pebble or Boulder ' + '(in ACMEv1 or ACMEv2 mode), Pebble is used if not set.') + parser.add_argument('--dns-server', '-d', + help='specify the DNS server as `IP:PORT` to use by ' + 'Pebble; if not specified, a local mock DNS server will be used to ' + 'resolve domains to localhost.') + args = parser.parse_args() + + acme_server = ACMEServer( + args.server_type, [], http_proxy=False, stdout=True, + dns_server=args.dns_server + ) try: with acme_server as acme_xdist: diff --git a/certbot-ci/certbot_integration_tests/utils/certbot_call.py b/certbot-ci/certbot_integration_tests/utils/certbot_call.py index a71c610e560..28aae322716 100755 --- a/certbot-ci/certbot_integration_tests/utils/certbot_call.py +++ b/certbot-ci/certbot_integration_tests/utils/certbot_call.py @@ -2,12 +2,13 @@ """Module to call certbot in test mode""" from __future__ import absolute_import -from distutils.version import LooseVersion import os import subprocess import sys +from distutils.version import LooseVersion import certbot_integration_tests +# pylint: disable=wildcard-import,unused-wildcard-import from certbot_integration_tests.utils.constants import * @@ -35,6 +36,8 @@ def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port, def _prepare_environ(workspace): + # pylint: disable=missing-function-docstring + new_environ = os.environ.copy() new_environ['TMPDIR'] = workspace @@ -58,8 +61,13 @@ def _prepare_environ(workspace): # certbot_integration_tests.__file__ is: # '/path/to/certbot/certbot-ci/certbot_integration_tests/__init__.pyc' # ... and we want '/path/to/certbot' - certbot_root = os.path.dirname(os.path.dirname(os.path.dirname(certbot_integration_tests.__file__))) - python_paths = [path for path in new_environ['PYTHONPATH'].split(':') if path != certbot_root] + certbot_root = os.path.dirname(os.path.dirname( + os.path.dirname(certbot_integration_tests.__file__)) + ) + python_paths = [ + path for path in new_environ['PYTHONPATH'].split(':') + if path != certbot_root + ] new_environ['PYTHONPATH'] = ':'.join(python_paths) return new_environ @@ -70,7 +78,8 @@ def _compute_additional_args(workspace, environ, force_renew): output = subprocess.check_output(['certbot', '--version'], universal_newlines=True, stderr=subprocess.STDOUT, cwd=workspace, env=environ) - version_str = output.split(' ')[1].strip() # Typical response is: output = 'certbot 0.31.0.dev0' + # Typical response is: output = 'certbot 0.31.0.dev0' + version_str = output.split(' ')[1].strip() if LooseVersion(version_str) >= LooseVersion('0.30.0'): additional_args.append('--no-random-sleep-on-renew') @@ -92,6 +101,7 @@ def _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_por '--no-verify-ssl', '--http-01-port', str(http_01_port), '--https-port', str(tls_alpn_01_port), + '--manual-public-ip-logging-ok', '--config-dir', config_dir, '--work-dir', os.path.join(workspace, 'work'), '--logs-dir', os.path.join(workspace, 'logs'), @@ -112,6 +122,7 @@ def _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_por def main(): + # pylint: disable=missing-function-docstring args = sys.argv[1:] # Default config is pebble diff --git a/certbot-ci/certbot_integration_tests/utils/constants.py b/certbot-ci/certbot_integration_tests/utils/constants.py index 8b002478ef6..81612ad537d 100644 --- a/certbot-ci/certbot_integration_tests/utils/constants.py +++ b/certbot-ci/certbot_integration_tests/utils/constants.py @@ -7,4 +7,4 @@ PEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir' PEBBLE_MANAGEMENT_URL = 'https://localhost:15000' MOCK_OCSP_SERVER_PORT = 4002 -PEBBLE_ALTERNATE_ROOTS = 2 \ No newline at end of file +PEBBLE_ALTERNATE_ROOTS = 2 diff --git a/certbot-ci/certbot_integration_tests/utils/dns_server.py b/certbot-ci/certbot_integration_tests/utils/dns_server.py new file mode 100644 index 00000000000..416f6567e61 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/utils/dns_server.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +"""Module to setup an RFC2136-capable DNS server""" +from __future__ import print_function + +import os +import os.path +import shutil +import socket +import subprocess +import sys +import tempfile +import time + +from pkg_resources import resource_filename + +BIND_DOCKER_IMAGE = "internetsystemsconsortium/bind9:9.16" +BIND_BIND_ADDRESS = ("127.0.0.1", 45953) + +# A TCP DNS message which is a query for '. CH A' transaction ID 0xcb37. This is used +# by _wait_until_ready to check that BIND is responding without depending on dnspython. +BIND_TEST_QUERY = bytearray.fromhex("0011cb37000000010000000000000000010003") + + +class DNSServer(object): + """ + DNSServer configures and handles the lifetime of an RFC2136-capable server. + DNServer provides access to the dns_xdist parameter, listing the address and port + to use for each pytest node. + + At this time, DNSServer should only be used with a single node, but may be expanded in + future to support parallelization (https://github.com/certbot/certbot/issues/8455). + """ + + def __init__(self, unused_nodes, show_output=False): + """ + Create an DNSServer instance. + :param list nodes: list of node names that will be setup by pytest xdist + :param bool show_output: if True, print the output of the DNS server + """ + + self.bind_root = tempfile.mkdtemp() + + self.process = None # type: subprocess.Popen + + self.dns_xdist = {"address": BIND_BIND_ADDRESS[0], "port": BIND_BIND_ADDRESS[1]} + + # Unfortunately the BIND9 image forces everything to stderr with -g and we can't + # modify the verbosity. + self._output = sys.stderr if show_output else open(os.devnull, "w") + + def start(self): + """Start the DNS server""" + try: + self._configure_bind() + self._start_bind() + except: + self.stop() + raise + + def stop(self): + """Stop the DNS server, and clean its resources""" + if self.process: + try: + self.process.terminate() + self.process.wait() + except BaseException as e: + print("BIND9 did not stop cleanly: {}".format(e), file=sys.stderr) + + shutil.rmtree(self.bind_root, ignore_errors=True) + + if self._output != sys.stderr: + self._output.close() + + def _configure_bind(self): + """Configure the BIND9 server based on the prebaked configuration""" + bind_conf_src = resource_filename( + "certbot_integration_tests", "assets/bind-config" + ) + for directory in ("conf", "zones"): + shutil.copytree( + os.path.join(bind_conf_src, directory), os.path.join(self.bind_root, directory) + ) + + def _start_bind(self): + """Launch the BIND9 server as a Docker container""" + addr_str = "{}:{}".format(BIND_BIND_ADDRESS[0], BIND_BIND_ADDRESS[1]) + self.process = subprocess.Popen( + [ + "docker", + "run", + "--rm", + "-p", + "{}:53/udp".format(addr_str), + "-p", + "{}:53/tcp".format(addr_str), + "-v", + "{}/conf:/etc/bind".format(self.bind_root), + "-v", + "{}/zones:/var/lib/bind".format(self.bind_root), + BIND_DOCKER_IMAGE, + ], + stdout=self._output, + stderr=self._output, + ) + + if self.process.poll(): + raise ValueError("BIND9 server stopped unexpectedly") + + try: + self._wait_until_ready() + except: + # The container might be running even if we think it isn't + self.stop() + raise + + def _wait_until_ready(self, attempts=30): + # type: (int) -> None + """ + Polls the DNS server over TCP until it gets a response, or until + it runs out of attempts and raises a ValueError. + The DNS response message must match the txn_id of the DNS query message, + but otherwise the contents are ignored. + :param int attempts: The number of attempts to make. + """ + for _ in range(attempts): + if self.process.poll(): + raise ValueError("BIND9 server stopped unexpectedly") + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(5.0) + try: + sock.connect(BIND_BIND_ADDRESS) + sock.sendall(BIND_TEST_QUERY) + buf = sock.recv(1024) + # We should receive a DNS message with the same tx_id + if buf and len(buf) > 4 and buf[2:4] == BIND_TEST_QUERY[2:4]: + return + # If we got a response but it wasn't the one we wanted, wait a little + time.sleep(1) + except: # pylint: disable=bare-except + # If there was a network error, wait a little + time.sleep(1) + finally: + sock.close() + + raise ValueError( + "Gave up waiting for DNS server {} to respond".format(BIND_BIND_ADDRESS) + ) + + def __enter__(self): + self.start() + return self.dns_xdist + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() diff --git a/certbot-ci/certbot_integration_tests/utils/misc.py b/certbot-ci/certbot_integration_tests/utils/misc.py index 38c2e60a82f..799b079fe0e 100644 --- a/certbot-ci/certbot_integration_tests/utils/misc.py +++ b/certbot-ci/certbot_integration_tests/utils/misc.py @@ -39,6 +39,7 @@ def _suppress_x509_verification_warnings(): urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) except ImportError: # Handle old versions of request with vendorized urllib3 + # pylint: disable=no-member from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) @@ -256,7 +257,8 @@ def generate_csr(domains, key_path, csr_path, key_type=RSA_KEY_TYPE): def read_certificate(cert_path): """ - Load the certificate from the provided path, and return a human readable version of it (TEXT mode). + Load the certificate from the provided path, and return a human readable version + of it (TEXT mode). :param str cert_path: the path to the certificate :returns: the TEXT version of the certificate, as it would be displayed by openssl binary """ @@ -280,7 +282,11 @@ def load_sample_data_path(workspace): if os.name == 'nt': # Fix the symlinks on Windows if GIT is not configured to create them upon checkout - for lineage in ['a.encryption-example.com', 'b.encryption-example.com']: + for lineage in [ + 'a.encryption-example.com', + 'b.encryption-example.com', + 'c.encryption-example.com', + ]: current_live = os.path.join(copied, 'live', lineage) for name in os.listdir(current_live): if name != 'README': diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py index 7fe03b99087..33ea6edcb16 100644 --- a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py +++ b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py @@ -1,3 +1,5 @@ +# pylint: disable=missing-module-docstring + import json import os import stat @@ -12,6 +14,7 @@ def fetch(workspace): + # pylint: disable=missing-function-docstring suffix = 'linux-amd64' if os.name != 'nt' else 'windows-amd64.exe' pebble_path = _fetch_asset('pebble', suffix) diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py b/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py index 9458560e848..b86e1cbc9f7 100755 --- a/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py +++ b/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py @@ -21,6 +21,7 @@ class _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler): + # pylint: disable=missing-function-docstring def do_POST(self): request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediate-keys/0', verify=False) issuer_key = serialization.load_pem_private_key(request.content, None, default_backend()) @@ -35,20 +36,28 @@ def do_POST(self): ocsp_request = ocsp.load_der_ocsp_request(self.rfile.read(content_len)) response = requests.get('{0}/cert-status-by-serial/{1}'.format( - PEBBLE_MANAGEMENT_URL, str(hex(ocsp_request.serial_number)).replace('0x', '')), verify=False) + PEBBLE_MANAGEMENT_URL, str(hex(ocsp_request.serial_number)).replace('0x', '')), + verify=False + ) if not response.ok: - ocsp_response = ocsp.OCSPResponseBuilder.build_unsuccessful(ocsp.OCSPResponseStatus.UNAUTHORIZED) + ocsp_response = ocsp.OCSPResponseBuilder.build_unsuccessful( + ocsp.OCSPResponseStatus.UNAUTHORIZED + ) else: data = response.json() now = datetime.datetime.utcnow() cert = x509.load_pem_x509_certificate(data['Certificate'].encode(), default_backend()) if data['Status'] != 'Revoked': - ocsp_status, revocation_time, revocation_reason = ocsp.OCSPCertStatus.GOOD, None, None + ocsp_status = ocsp.OCSPCertStatus.GOOD + revocation_time = None + revocation_reason = None else: - ocsp_status, revocation_reason = ocsp.OCSPCertStatus.REVOKED, x509.ReasonFlags.unspecified - revoked_at = re.sub(r'( \+\d{4}).*$', r'\1', data['RevokedAt']) # "... +0000 UTC" => "+0000" + ocsp_status = ocsp.OCSPCertStatus.REVOKED + revocation_reason = x509.ReasonFlags.unspecified + # "... +0000 UTC" => "+0000" + revoked_at = re.sub(r'( \+\d{4}).*$', r'\1', data['RevokedAt']) revocation_time = parser.parse(revoked_at) ocsp_response = ocsp.OCSPResponseBuilder().add_response( diff --git a/certbot-ci/certbot_integration_tests/utils/proxy.py b/certbot-ci/certbot_integration_tests/utils/proxy.py index 3a16adebfa8..225f98e6e17 100644 --- a/certbot-ci/certbot_integration_tests/utils/proxy.py +++ b/certbot-ci/certbot_integration_tests/utils/proxy.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# pylint: disable=missing-module-docstring + import json import re import sys @@ -10,7 +12,9 @@ def _create_proxy(mapping): + # pylint: disable=missing-function-docstring class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler): + # pylint: disable=missing-class-docstring def do_GET(self): headers = {key.lower(): value for key, value in self.headers.items()} backend = [backend for pattern, backend in mapping.items() diff --git a/certbot-ci/setup.py b/certbot-ci/setup.py index 971db2b8ed9..ce29fe45d64 100644 --- a/certbot-ci/setup.py +++ b/certbot-ci/setup.py @@ -52,6 +52,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], diff --git a/certbot-ci/windows_installer_integration_tests/conftest.py b/certbot-ci/windows_installer_integration_tests/conftest.py index e36654f90e5..c6a89c323d7 100644 --- a/certbot-ci/windows_installer_integration_tests/conftest.py +++ b/certbot-ci/windows_installer_integration_tests/conftest.py @@ -9,8 +9,6 @@ from __future__ import print_function import os -import pytest - ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 598454ae8b5..c894e5dee4f 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -5,7 +5,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' install_requires = [ 'certbot', @@ -50,6 +50,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 96db7351a1b..a00f06a8abf 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -63,6 +63,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 09225a42ed5..3771c1d34d2 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -63,6 +63,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 4da16114673..f168ee06af8 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -64,6 +64,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index cd1246e07df..f23bd66686f 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -74,6 +74,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index c077acbef26..e654ed42125 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -63,6 +63,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py index ba58509f7dd..a856f1cdefe 100644 --- a/certbot-dns-gehirn/setup.py +++ b/certbot-dns-gehirn/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -62,6 +62,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-google/certbot_dns_google/_internal/dns_google.py b/certbot-dns-google/certbot_dns_google/_internal/dns_google.py index 4eaed178309..1bd3468daca 100644 --- a/certbot-dns-google/certbot_dns_google/_internal/dns_google.py +++ b/certbot-dns-google/certbot_dns_google/_internal/dns_google.py @@ -85,9 +85,13 @@ def __init__(self, account_json=None, dns_api=None): scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite'] if account_json is not None: - credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes) - with open(account_json) as account: - self.project_id = json.load(account)['project_id'] + try: + credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes) + with open(account_json) as account: + self.project_id = json.load(account)['project_id'] + except Exception as e: + raise errors.PluginError( + "Error parsing credentials file '{}': {}".format(account_json, e)) else: credentials = None self.project_id = self.get_project_id() diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 78a78cbc3f4..82c2a9102a3 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -66,6 +66,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-google/tests/dns_google_test.py b/certbot-dns-google/tests/dns_google_test.py index 5af027cef6b..40002f14372 100644 --- a/certbot-dns-google/tests/dns_google_test.py +++ b/certbot-dns-google/tests/dns_google_test.py @@ -107,6 +107,17 @@ def test_client_without_credentials(self, get_project_id_mock, credential_mock, self.assertFalse(credential_mock.called) self.assertTrue(get_project_id_mock.called) + @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') + def test_client_bad_credentials_file(self, credential_mock): + credential_mock.side_effect = ValueError('Some exception buried in oauth2client') + with self.assertRaises(errors.PluginError) as cm: + self._setUp_client_with_mock([]) + self.assertEqual( + str(cm.exception), + "Error parsing credentials file '/not/a/real/path.json': " + "Some exception buried in oauth2client" + ) + @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google._internal.dns_google.open', mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index 4be13cf5649..a6f159757b4 100644 --- a/certbot-dns-linode/setup.py +++ b/certbot-dns-linode/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -62,6 +62,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index 80515877145..ff4a1b41d8c 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -63,6 +63,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 100a4855156..887d5120a69 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -63,6 +63,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py index 585b038a369..d519a9e18a6 100644 --- a/certbot-dns-ovh/setup.py +++ b/certbot-dns-ovh/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -63,6 +63,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py index 57e9506f2ea..a3a9436601c 100644 --- a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py @@ -1,3 +1,13 @@ +# type: ignore +# pylint: disable=no-member +# Many attributes of dnspython are now dynamically defined which causes both +# mypy and pylint to error about accessing attributes they think do not exist. +# This is the case even in up-to-date versions of mypy and pylint which as of +# writing this are 0.790 and 2.6.0 respectively. This problem may be fixed in +# dnspython 2.1.0. See https://github.com/rthalley/dnspython/issues/598. For +# now, let's disable these checks. This is done at the very top of the file +# like this because "type: ignore" must be the first line in the file to be +# respected by mypy. """DNS Authenticator using RFC 2136 Dynamic Updates.""" import logging diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index c841aefc1e2..540fc1a67d7 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -63,6 +63,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-rfc2136/tests/dns_rfc2136_test.py b/certbot-dns-rfc2136/tests/dns_rfc2136_test.py index de5e2912b92..dc4a73a04e4 100644 --- a/certbot-dns-rfc2136/tests/dns_rfc2136_test.py +++ b/certbot-dns-rfc2136/tests/dns_rfc2136_test.py @@ -154,7 +154,7 @@ def test_find_domain(self): # _find_domain | pylint: disable=protected-access domain = self.rfc2136_client._find_domain('foo.bar.'+DOMAIN) - self.assertTrue(domain == DOMAIN) + self.assertEqual(domain, DOMAIN) def test_find_domain_wraps_errors(self): # _query_soa | pylint: disable=protected-access diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 2ad0de04500..cffa163673f 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -58,6 +58,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py index b1c6ee6a370..2c88f1226f3 100644 --- a/certbot-dns-sakuracloud/setup.py +++ b/certbot-dns-sakuracloud/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -62,6 +62,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-nginx/certbot_nginx/_internal/http_01.py b/certbot-nginx/certbot_nginx/_internal/http_01.py index 2f6458f87da..40f994988ab 100644 --- a/certbot-nginx/certbot_nginx/_internal/http_01.py +++ b/certbot-nginx/certbot_nginx/_internal/http_01.py @@ -1,5 +1,6 @@ """A class that performs HTTP-01 challenges for Nginx""" +import io import logging from acme import challenges @@ -102,7 +103,7 @@ def _mod_config(self): self.configurator.reverter.register_file_creation( True, self.challenge_conf) - with open(self.challenge_conf, "w") as new_conf: + with io.open(self.challenge_conf, "w", encoding="utf-8") as new_conf: nginxparser.dump(config, new_conf) def _default_listen_addresses(self): diff --git a/certbot-nginx/certbot_nginx/_internal/nginxparser.py b/certbot-nginx/certbot_nginx/_internal/nginxparser.py index a8ac9042702..5f723dcefec 100644 --- a/certbot-nginx/certbot_nginx/_internal/nginxparser.py +++ b/certbot-nginx/certbot_nginx/_internal/nginxparser.py @@ -16,6 +16,7 @@ from pyparsing import White from pyparsing import ZeroOrMore import six +from acme.magic_typing import IO, Any # pylint: disable=unused-import logger = logging.getLogger(__name__) @@ -130,26 +131,27 @@ def load(_file): def dumps(blocks): - """Dump to a string. + # type: (UnspacedList) -> six.text_type + """Dump to a Unicode string. :param UnspacedList block: The parsed tree - :param int indentation: The number of spaces to indent - :rtype: str + :rtype: six.text_type """ - return str(RawNginxDumper(blocks.spaced)) + return six.text_type(RawNginxDumper(blocks.spaced)) def dump(blocks, _file): + # type: (UnspacedList, IO[Any]) -> None """Dump to a file. :param UnspacedList block: The parsed tree - :param file _file: The file to dump to - :param int indentation: The number of spaces to indent - :rtype: NoneType + :param IO[Any] _file: The file stream to dump to. It must be opened with + Unicode encoding. + :rtype: None """ - return _file.write(dumps(blocks)) + _file.write(dumps(blocks)) spacey = lambda x: (isinstance(x, six.string_types) and x.isspace()) or x == '' diff --git a/certbot-nginx/certbot_nginx/_internal/parser.py b/certbot-nginx/certbot_nginx/_internal/parser.py index 641ffb0206f..72091b03fc9 100644 --- a/certbot-nginx/certbot_nginx/_internal/parser.py +++ b/certbot-nginx/certbot_nginx/_internal/parser.py @@ -249,7 +249,7 @@ def filedump(self, ext='tmp', lazy=True): continue out = nginxparser.dumps(tree) logger.debug('Writing nginx conf tree to %s:\n%s', filename, out) - with open(filename, 'w') as _file: + with io.open(filename, 'w', encoding='utf-8') as _file: _file.write(out) except IOError: diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index a4faf470ee7..0ed164da2e1 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -5,7 +5,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.10.0.dev0' +version = '1.11.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -49,6 +49,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-nginx/tests/configurator_test.py b/certbot-nginx/tests/configurator_test.py index e677ad471cd..5e788e394e8 100644 --- a/certbot-nginx/tests/configurator_test.py +++ b/certbot-nginx/tests/configurator_test.py @@ -842,7 +842,7 @@ def test_parser_reload_after_config_changes(self, mock_parser_load, unused_mock_ self.config.recovery_routine() self.config.revert_challenge_config() self.config.rollback_checkpoints() - self.assertTrue(mock_parser_load.call_count == 3) + self.assertEqual(mock_parser_load.call_count, 3) def test_choose_vhosts_wildcard(self): # pylint: disable=protected-access diff --git a/certbot-nginx/tests/obj_test.py b/certbot-nginx/tests/obj_test.py index 93b9493ebf4..f385649ed69 100644 --- a/certbot-nginx/tests/obj_test.py +++ b/certbot-nginx/tests/obj_test.py @@ -75,7 +75,7 @@ def test_eq(self): new_addr1 = Addr.fromstring("192.168.1.1 spdy") self.assertEqual(self.addr1, new_addr1) self.assertNotEqual(self.addr1, self.addr2) - self.assertFalse(self.addr1 == 3333) + self.assertNotEqual(self.addr1, 3333) def test_equivalent_any_addresses(self): from certbot_nginx._internal.obj import Addr @@ -168,7 +168,7 @@ def test_eq(self): self.assertEqual(vhost1b, self.vhost1) self.assertEqual(str(vhost1b), str(self.vhost1)) - self.assertFalse(vhost1b == 1234) + self.assertNotEqual(vhost1b, 1234) def test_str(self): stringified = '\n'.join(['file: filep', 'addrs: localhost', diff --git a/certbot-nginx/tests/parser_test.py b/certbot-nginx/tests/parser_test.py index 620d1b6de11..0083c2448bb 100644 --- a/certbot-nginx/tests/parser_test.py +++ b/certbot-nginx/tests/parser_test.py @@ -492,6 +492,14 @@ def test_valid_unicode_characters(self): self.assertEqual(['server'], parsed[0][2][0]) self.assertEqual(['listen', '80'], parsed[0][2][1][3]) + def test_valid_unicode_roundtrip(self): + """This tests the parser's ability to load and save a config containing Unicode""" + nparser = parser.NginxParser(self.config_path) + nparser._parse_files( + nparser.abs_path('valid_unicode_comments.conf') + ) # pylint: disable=protected-access + nparser.filedump(lazy=False) + def test_invalid_unicode_characters(self): with self.assertLogs() as log: nparser = parser.NginxParser(self.config_path) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index fc83d3b46bf..e4f4eda5141 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -2,24 +2,61 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). -## 1.10.0 - master +## 1.11.0 - master + +### Added + +* + +### Changed + +* We deprecated support for Python 2 in Certbot and its ACME library. + Support for Python 2 will be removed in the next planned release of Certbot. +* certbot-auto was deprecated on all systems. For more information about this + change, see + https://community.letsencrypt.org/t/certbot-auto-no-longer-works-on-debian-based-systems/139702/7. +* We deprecated support for Apache 2.2 in the certbot-apache plugin and it will + be removed in a future release of Certbot. + +### Fixed + +* The Certbot snap no longer loads packages installed via `pip install --user`. This + was unintended and DNS plugins should be installed via `snap` instead. + +More details about these changes can be found on our GitHub repo. + +## 1.10.1 - 2020-12-03 + +### Fixed + +* Fixed a bug in `certbot.util.add_deprecated_argument` that caused the + deprecated `--manual-public-ip-logging-ok` flag to crash Certbot in some + scenarios. + +More details about these changes can be found on our GitHub repo. + +## 1.10.0 - 2020-12-01 ### Added * Added timeout to DNS query function calls for dns-rfc2136 plugin. * Confirmation when deleting certificates -* +* CLI flag `--key-type` has been added to specify 'rsa' or 'ecdsa' (default 'rsa'). +* CLI flag `--elliptic-curve` has been added which takes an NIST/SECG elliptic curve. Any of + `secp256r1`, `secp384r1` and `secp521r1` are accepted values. +* The command `certbot certficates` lists the which type of the private key that was used + for the private key. +* Support for Python 3.9 was added to Certbot and all of its components. ### Changed * certbot-auto was deprecated on Debian based systems. * CLI flag `--manual-public-ip-logging-ok` is now a no-op, generates a deprecation warning, and will be removed in a future release. -* ### Fixed -* +* Fixed a Unicode-related crash in the nginx plugin when running under Python 2. More details about these changes can be found on our GitHub repo. @@ -55,7 +92,7 @@ More details about these changes can be found on our GitHub repo. ### Added -* Added the ability to remove email and phone contact information from an account +* Added the ability to remove email and phone contact information from an account using `update_account --register-unsafely-without-email` ### Changed @@ -67,7 +104,7 @@ More details about these changes can be found on our GitHub repo. * The problem causing the Apache plugin in the Certbot snap on ARM systems to fail to load the Augeas library it depends on has been fixed. * The `acme` library can now tell the ACME server to clear contact information by passing an empty - `tuple` to the `contact` field of a `Registration` message. + `tuple` to the `contact` field of a `Registration` message. * Fixed the `*** stack smashing detected ***` error in the Certbot snap on some systems. More details about these changes can be found on our GitHub repo. diff --git a/certbot/README.rst b/certbot/README.rst index 6d04e6fa314..0cffc57a71d 100644 --- a/certbot/README.rst +++ b/certbot/README.rst @@ -18,10 +18,6 @@ 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. - Contributing ------------ @@ -106,6 +102,8 @@ Current Features * Can get domain-validated (DV) certificates. * Can revoke certificates. * Adjustable RSA key bit-length (2048 (default), 4096, ...). +* Adjustable `EC `_ + key (`secp256r1` (default), `secp384r1`, `secp521r1`). * Can optionally install a http -> https redirect, so your site effectively runs https only (Apache only) * Fully automated. diff --git a/certbot/certbot/__init__.py b/certbot/certbot/__init__.py index 8ee4156824d..98009a71b7a 100644 --- a/certbot/certbot/__init__.py +++ b/certbot/certbot/__init__.py @@ -1,4 +1,13 @@ """Certbot client.""" +import warnings +import sys # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '1.10.0.dev0' +__version__ = '1.11.0.dev0' + +if sys.version_info[0] == 2: + warnings.warn( + "Python 2 support will be dropped in the next release of Certbot. " + "Please upgrade your Python version.", + PendingDeprecationWarning, + ) # pragma: no cover diff --git a/certbot/certbot/_internal/cert_manager.py b/certbot/certbot/_internal/cert_manager.py index 5c1c515c2fb..80a98ab04a8 100644 --- a/certbot/certbot/_internal/cert_manager.py +++ b/certbot/certbot/_internal/cert_manager.py @@ -65,6 +65,7 @@ def rename_lineage(config): disp.notification("Successfully renamed {0} to {1}." .format(certname, new_certname), pause=False) + def certificates(config): """Display information about certs configured with Certbot @@ -87,6 +88,7 @@ def certificates(config): # Describe all the certs _describe_certs(config, parsed_certs, parse_failures) + def delete(config): """Delete Certbot files associated with a certificate lineage.""" certnames = get_certnames(config, "delete", allow_multiple=True) @@ -123,11 +125,13 @@ def lineage_for_certname(cli_config, certname): 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.""" lineage = lineage_for_certname(config, certname) return lineage.names() if lineage else None + def find_duplicative_certs(config, domains): """Find existing certs that match the given domain names. @@ -172,6 +176,7 @@ def update_certs_for_domain_matches(candidate_lineage, rv): 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. @@ -193,6 +198,7 @@ def _archive_files(candidate_lineage, filetype): return pattern 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. @@ -203,6 +209,7 @@ def _acceptable_matches(): 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. @@ -219,6 +226,7 @@ def cert_path_to_lineage(cli_config): 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 @@ -284,20 +292,23 @@ def human_readable_cert_info(config, cert, skip_filter_checks=False): valid_string = "{0} ({1})".format(cert.target_expiry, status) serial = format(crypto_util.get_serial_from_cert(cert.cert_path), 'x') - certinfo.append(" Certificate Name: {0}\n" - " Serial Number: {1}\n" - " Domains: {2}\n" - " Expiry Date: {3}\n" - " Certificate Path: {4}\n" - " Private Key Path: {5}".format( + certinfo.append(" Certificate Name: {}\n" + " Serial Number: {}\n" + " Key Type: {}\n" + " Domains: {}\n" + " Expiry Date: {}\n" + " Certificate Path: {}\n" + " Private Key Path: {}".format( cert.lineagename, serial, + cert.private_key_type, " ".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. """ @@ -337,10 +348,12 @@ def get_certnames(config, verb, allow_multiple=False, custom_prompt=None): # Private Helpers ################### + def _report_lines(msgs): """Format a results report for a category of single-line renewal outcomes""" return " " + "\n ".join(str(msg) for msg in msgs) + def _report_human_readable(config, parsed_certs): """Format a results report for a parsed cert""" certinfo = [] @@ -348,6 +361,7 @@ def _report_human_readable(config, parsed_certs): 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 = [] # type: List[str] @@ -369,6 +383,7 @@ def _describe_certs(config, parsed_certs, 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, *args): """Iterate func over unbroken lineages, allowing custom return conditions. diff --git a/certbot/certbot/_internal/cli/__init__.py b/certbot/certbot/_internal/cli/__init__.py index f6f648aa917..e50cb338a76 100644 --- a/certbot/certbot/_internal/cli/__init__.py +++ b/certbot/certbot/_internal/cli/__init__.py @@ -313,6 +313,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): helpful.add( "security", "--rsa-key-size", type=int, metavar="N", default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) + helpful.add( + "security", "--key-type", choices=['rsa', 'ecdsa'], type=str, + default=flag_default("key_type"), help=config_help("key_type")) + helpful.add( + "security", "--elliptic-curve", type=str, choices=[ + 'secp256r1', + 'secp384r1', + 'secp521r1', + ], metavar="N", + default=flag_default("elliptic_curve"), help=config_help("elliptic_curve")) helpful.add( "security", "--must-staple", action="store_true", dest="must_staple", default=flag_default("must_staple"), diff --git a/certbot/certbot/_internal/cli/helpful.py b/certbot/certbot/_internal/cli/helpful.py index a86ff3743e2..2cb5061573b 100644 --- a/certbot/certbot/_internal/cli/helpful.py +++ b/certbot/certbot/_internal/cli/helpful.py @@ -2,8 +2,10 @@ from __future__ import print_function import argparse import copy +import functools import glob import sys + import configargparse import six import zope.component @@ -230,6 +232,10 @@ def parse_args(self): raise errors.Error( "Parameters --hsts and --auto-hsts cannot be used simultaneously.") + if isinstance(parsed_args.key_type, list) and len(parsed_args.key_type) > 1: + raise errors.Error( + "Only *one* --key-type type may be provided at this time.") + return parsed_args def set_test_server(self, parsed_args): @@ -352,6 +358,18 @@ def add(self, topics, *args, **kwargs): :param dict **kwargs: various argparse settings for this argument """ + action = kwargs.get("action") + if action is util.DeprecatedArgumentAction: + # If the argument is deprecated through + # certbot.util.add_deprecated_argument, it is not shown in the help + # output and any value given to the argument is thrown away during + # argument parsing. Because of this, we handle this case early + # skipping putting the argument in different help topics and + # handling default detection since these actions aren't needed and + # can cause bugs like + # https://github.com/certbot/certbot/issues/8495. + self.parser.add_argument(*args, **kwargs) + return if isinstance(topics, list): # if this flag can be listed in multiple sections, try to pick the one @@ -406,8 +424,22 @@ def add_deprecated_argument(self, argument_name, num_args): :param int nargs: Number of arguments the option takes. """ - util.add_deprecated_argument( - self.parser.add_argument, argument_name, num_args) + # certbot.util.add_deprecated_argument expects the normal add_argument + # interface provided by argparse. This is what is given including when + # certbot.util.add_deprecated_argument is used by plugins, however, in + # that case the first argument to certbot.util.add_deprecated_argument + # is certbot._internal.cli.HelpfulArgumentGroup.add_argument which + # internally calls the add method of this class. + # + # The difference between the add method of this class and the standard + # argparse add_argument method caused a bug in the past (see + # https://github.com/certbot/certbot/issues/8495) so we use the same + # code path here for consistency and to ensure it works. To do that, we + # wrap the add method in a similar way to + # HelpfulArgumentGroup.add_argument by providing a help topic (which in + # this case is set to None). + add_func = functools.partial(self.add, None) + util.add_deprecated_argument(add_func, argument_name, num_args) def add_group(self, topic, verbs=(), **kwargs): """Create a new argument group. diff --git a/certbot/certbot/_internal/client.py b/certbot/certbot/_internal/client.py index b198a8c27e9..5dc62580e17 100644 --- a/certbot/certbot/_internal/client.py +++ b/certbot/certbot/_internal/client.py @@ -312,7 +312,6 @@ def obtain_certificate(self, domains, old_keypath=None): :rtype: tuple """ - # We need to determine the key path, key PEM data, CSR path, # and CSR PEM data. For a dry run, the paths are None because # they aren't permanently saved to disk. For a lineage with @@ -335,16 +334,41 @@ def obtain_certificate(self, domains, old_keypath=None): # The key is set to None here but will be created below. key = None + key_size = self.config.rsa_key_size + elliptic_curve = None + + # key-type defaults to a list, but we are only handling 1 currently + if isinstance(self.config.key_type, list): + self.config.key_type = self.config.key_type[0] + if self.config.elliptic_curve and self.config.key_type == 'ecdsa': + elliptic_curve = self.config.elliptic_curve + self.config.auth_chain_path = "./chain-ecdsa.pem" + self.config.auth_cert_path = "./cert-ecdsa.pem" + self.config.key_path = "./key-ecdsa.pem" + elif self.config.rsa_key_size and self.config.key_type.lower() == 'rsa': + key_size = self.config.rsa_key_size + # Create CSR from names if self.config.dry_run: - key = key or util.Key(file=None, - pem=crypto_util.make_key(self.config.rsa_key_size)) + key = key or util.Key( + file=None, + pem=crypto_util.make_key( + bits=key_size, + elliptic_curve=elliptic_curve, + key_type=self.config.key_type, + + ), + ) csr = util.CSR(file=None, form="pem", data=acme_crypto_util.make_csr( key.pem, domains, self.config.must_staple)) else: - key = key or crypto_util.init_save_key(self.config.rsa_key_size, - self.config.key_dir) + key = key or crypto_util.init_save_key( + key_size=key_size, + key_dir=self.config.key_dir, + key_type=self.config.key_type, + elliptic_curve=elliptic_curve, + ) csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names) diff --git a/certbot/certbot/_internal/constants.py b/certbot/certbot/_internal/constants.py index 9e9373f1368..f79d6b9dbff 100644 --- a/certbot/certbot/_internal/constants.py +++ b/certbot/certbot/_internal/constants.py @@ -57,6 +57,8 @@ https_port=443, break_my_certs=False, rsa_key_size=2048, + elliptic_curve="secp256r1", + key_type="rsa", must_staple=False, redirect=None, auto_hsts=False, diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py index e02737c4c4b..46ece86c5db 100644 --- a/certbot/certbot/_internal/main.py +++ b/certbot/certbot/_internal/main.py @@ -5,13 +5,14 @@ import functools import logging.handlers import sys +import warnings import configobj import josepy as jose import zope.component from acme import errors as acme_errors -from acme.magic_typing import Union, Iterable, Optional # pylint: disable=unused-import +from acme.magic_typing import Union, Iterable, Optional, List, Tuple # pylint: disable=unused-import import certbot from certbot import crypto_util from certbot import errors @@ -113,12 +114,24 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N if lineage is not None: # Renewal, where we already know the specific lineage we're # interested in - logger.info("Renewing an existing certificate") + display_util.notify( + "{action} for {domains}".format( + action="Simulating renewal of an existing certificate" + if config.dry_run else "Renewing an existing certificate", + domains=display_util.summarize_domain_list(domains or lineage.names()) + ) + ) renewal.renew_cert(config, domains, le_client, lineage) else: # TREAT AS NEW REQUEST assert domains is not None - logger.info("Obtaining a new certificate") + display_util.notify( + "{action} for {domains}".format( + action="Simulating a certificate request" if config.dry_run else + "Requesting a certificate", + domains=display_util.summarize_domain_list(domains) + ) + ) lineage = le_client.obtain_and_enroll_certificate(domains, certname) if lineage is False: raise errors.Error("Certificate could not be obtained") @@ -130,7 +143,33 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N return lineage -def _handle_subset_cert_request(config, domains, cert): +def _handle_unexpected_key_type_migration(config, cert): + # type: (configuration.NamespaceConfig, storage.RenewableCert) -> None + """ + This function ensures that the user will not implicitly migrate an existing key + from one type to another in the situation where a certificate for that lineage + already exist and they have not provided explicitly --key-type and --cert-name. + :param config: Current configuration provided by the client + :param cert: Matching certificate that could be renewed + """ + if not cli.set_by_cli("key_type") or not cli.set_by_cli("certname"): + + new_key_type = config.key_type.upper() + cur_key_type = cert.private_key_type.upper() + + if new_key_type != cur_key_type: + msg = ('Are you trying to change the key type of the certificate named {0} ' + 'from {1} to {2}? Please provide both --cert-name and --key-type on ' + 'the command line confirm the change you are trying to make.') + msg = msg.format(cert.lineagename, cur_key_type, new_key_type) + raise errors.Error(msg) + + +def _handle_subset_cert_request(config, # type: configuration.NamespaceConfig + domains, # type: List[str] + cert # type: storage.RenewableCert + ): + # type: (...) -> Tuple[str, Optional[storage.RenewableCert]] """Figure out what to do if a previous cert had a subset of the names now requested :param config: Configuration object @@ -147,6 +186,8 @@ def _handle_subset_cert_request(config, domains, cert): :rtype: `tuple` of `str` """ + _handle_unexpected_key_type_migration(config, cert) + existing = ", ".join(cert.names()) question = ( "You have an existing certificate that contains a portion of " @@ -175,7 +216,10 @@ def _handle_subset_cert_request(config, domains, cert): raise errors.Error(USER_CANCELLED) -def _handle_identical_cert_request(config, lineage): +def _handle_identical_cert_request(config, # type: configuration.NamespaceConfig + lineage, # type: storage.RenewableCert + ): + # type: (...) -> Tuple[str, Optional[storage.RenewableCert]] """Figure out what to do if a lineage has the same names as a previously obtained one :param config: Configuration object @@ -189,6 +233,8 @@ def _handle_identical_cert_request(config, lineage): :rtype: `tuple` of `str` """ + _handle_unexpected_key_type_migration(config, lineage) + if not lineage.ensure_deployed(): return "reinstall", lineage if renewal.should_renew(config, lineage): @@ -264,6 +310,7 @@ def _find_lineage_for_domains(config, domains): return _handle_subset_cert_request(config, domains, subset_names_cert) return None, None + def _find_cert(config, domains, certname): """Finds an existing certificate object given domains and/or a certificate name. @@ -287,7 +334,12 @@ def _find_cert(config, domains, certname): logger.info("Keeping the existing certificate") return (action != "reinstall"), lineage -def _find_lineage_for_domains_and_certname(config, domains, certname): + +def _find_lineage_for_domains_and_certname(config, # type: configuration.NamespaceConfig + domains, # type: List[str] + certname # type: str + ): + # type: (...) -> Tuple[str, Optional[storage.RenewableCert]] """Find appropriate lineage based on given domains and/or certname. :param config: Configuration object @@ -314,8 +366,9 @@ def _find_lineage_for_domains_and_certname(config, domains, certname): if lineage: if domains: if set(cert_manager.domains_for_certname(config, certname)) != set(domains): + _handle_unexpected_key_type_migration(config, lineage) _ask_user_to_confirm_new_names(config, domains, certname, - lineage.names()) # raises if no + lineage.names()) # raises if no return "renew", lineage # unnecessarily specified domains or no domains specified return _handle_identical_cert_request(config, lineage) @@ -384,6 +437,7 @@ def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): 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, question=None): """Retrieve domains and certname from config or user input. @@ -1002,6 +1056,7 @@ def delete(config, unused_plugins): """ cert_manager.delete(config) + def certificates(config, unused_plugins): """Display information about certs configured with Certbot @@ -1017,6 +1072,7 @@ def certificates(config, unused_plugins): """ cert_manager.certificates(config) + # TODO: coop with renewal config def revoke(config, unused_plugins): """Revoke a previously obtained certificate. @@ -1149,6 +1205,7 @@ def _csr_get_and_save_cert(config, le_client): os.path.normpath(config.chain_path), os.path.normpath(config.fullchain_path)) return cert_path, fullchain_path + def renew_cert(config, plugins, lineage): """Renew & save an existing cert. Do not install it. @@ -1325,6 +1382,7 @@ def main(cli_args=None): plugins = plugins_disco.PluginsRegistry.find_all() logger.debug("certbot version: %s", certbot.__version__) + logger.debug("Location of certbot entry point: %s", sys.argv[0]) # do not log `config`, as it contains sensitive data (e.g. revoke --key)! logger.debug("Arguments: %r", cli_args) logger.debug("Discovered plugins: %r", plugins) @@ -1346,6 +1404,13 @@ def main(cli_args=None): if config.func != plugins_cmd: # pylint: disable=comparison-with-callable raise + if sys.version_info[0] == 2: + warnings.warn( + "Python 2 support will be dropped in the next release of Certbot. " + "Please upgrade your Python version.", + PendingDeprecationWarning, + ) # pragma: no cover + set_displayer(config) # Reporter diff --git a/certbot/certbot/_internal/plugins/null.py b/certbot/certbot/_internal/plugins/null.py index cf7c05a2b73..5ab2f4a04df 100644 --- a/certbot/certbot/_internal/plugins/null.py +++ b/certbot/certbot/_internal/plugins/null.py @@ -1,7 +1,6 @@ """Null plugin.""" import logging -import zope.component import zope.interface from certbot import interfaces diff --git a/certbot/certbot/_internal/renewal.py b/certbot/certbot/_internal/renewal.py index 50e4d3a4ce1..9b528cb6a18 100644 --- a/certbot/certbot/_internal/renewal.py +++ b/certbot/certbot/_internal/renewal.py @@ -10,7 +10,7 @@ import traceback from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.hazmat.primitives.serialization import load_pem_private_key import OpenSSL import six @@ -19,6 +19,7 @@ from acme.magic_typing import List from acme.magic_typing import Optional # pylint: disable=unused-import from certbot import crypto_util +from certbot.display import util as display_util from certbot import errors from certbot import interfaces from certbot import util @@ -40,7 +41,7 @@ STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent", "server", "account", "authenticator", "installer", "renew_hook", "pre_hook", "post_hook", "http01_address", - "preferred_chain"] + "preferred_chain", "key_type", "elliptic_curve"] INT_CONFIG_ITEMS = ["rsa_key_size", "http01_port"] BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names", "reuse_key", "autorenew"] @@ -347,40 +348,42 @@ def report(msgs, category): def _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures): + # type: (interfaces.IConfig, List[str], List[str], List[str], List[str]) -> None + """ + Print a report to the terminal about the results of the renewal process. - out = [] # type: List[str] - notify = out.append - disp = zope.component.getUtility(interfaces.IDisplay) + :param interfaces.IConfig config: Configuration + :param list renew_successes: list of fullchain paths which were renewed + :param list renew_failures: list of fullchain paths which failed to be renewed + :param list renew_skipped: list of messages to print about skipped certificates + :param list parse_failures: list of renewal parameter paths which had erorrs + """ + notify = display_util.notify + notify_error = logger.error - def notify_error(err): - """Notify and log errors.""" - notify(str(err)) - logger.error(err) + notify('\n{}'.format(display_util.SIDE_FRAME)) + + renewal_noun = "simulated renewal" if config.dry_run else "renewal" - if config.dry_run: - notify("** DRY RUN: simulating 'certbot renew' close to cert expiry") - notify("** (The test certificates below have not been saved.)") - notify("") if renew_skipped: notify("The following certs are not due for renewal yet:") notify(report(renew_skipped, "skipped")) if not renew_successes and not renew_failures: - notify("No renewals were attempted.") + notify("No {renewal}s were attempted.".format(renewal=renewal_noun)) if (config.pre_hook is not None or config.renew_hook is not None or config.post_hook is not None): notify("No hooks were run.") elif renew_successes and not renew_failures: - notify("Congratulations, all renewals succeeded. The following certs " - "have been renewed:") + notify("Congratulations, all {renewal}s succeeded: ".format(renewal=renewal_noun)) notify(report(renew_successes, "success")) elif renew_failures and not renew_successes: - notify_error("All renewal attempts failed. The following certs could " - "not be renewed:") + notify_error("All %ss failed. The following certs could " + "not be renewed:", renewal_noun) notify_error(report(renew_failures, "failure")) elif renew_failures and renew_successes: - notify("The following certs were successfully renewed:") + notify("The following {renewal}s succeeded:".format(renewal=renewal_noun)) notify(report(renew_successes, "success") + "\n") - notify_error("The following certs could not be renewed:") + notify_error("The following %ss failed:", renewal_noun) notify_error(report(renew_failures, "failure")) if parse_failures: @@ -388,11 +391,7 @@ def notify_error(err): "were invalid: ") notify(report(parse_failures, "parsefail")) - if config.dry_run: - notify("** DRY RUN: simulating 'certbot renew' close to cert expiry") - notify("** (The test certificates above have not been saved.)") - - disp.notification("\n".join(out), wrap=False) + notify(display_util.SIDE_FRAME) def handle_renewal_request(config): @@ -482,9 +481,10 @@ def handle_renewal_request(config): except Exception as e: # pylint: disable=broad-except # obtain_cert (presumably) encountered an unanticipated problem. - logger.warning("Attempting to renew cert (%s) from %s produced an " - "unexpected error: %s. Skipping.", lineagename, - renewal_file, e) + logger.error( + "Failed to renew cert %s with error: %s", + lineagename, e + ) logger.debug("Traceback was:\n%s", traceback.format_exc()) renew_failures.append(renewal_candidate.fullchain) @@ -506,6 +506,10 @@ def _update_renewal_params_from_key(key_path, config): with open(key_path, 'rb') as file_h: key = load_pem_private_key(file_h.read(), password=None, backend=default_backend()) if isinstance(key, rsa.RSAPrivateKey): + config.key_type = 'rsa' config.rsa_key_size = key.key_size + elif isinstance(key, ec.EllipticCurvePrivateKey): + config.key_type = 'ecdsa' + config.elliptic_curve = key.curve.name else: raise errors.Error('Key at {0} is of an unsupported type: {1}.'.format(key_path, type(key))) diff --git a/certbot/certbot/_internal/storage.py b/certbot/certbot/_internal/storage.py index 84bd3314318..b6c37a5ba32 100644 --- a/certbot/certbot/_internal/storage.py +++ b/certbot/certbot/_internal/storage.py @@ -10,6 +10,9 @@ import parsedatetime import pytz import six +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.serialization import load_pem_private_key import certbot from certbot import crypto_util @@ -46,6 +49,7 @@ def renewal_conf_files(config): result.sort() return result + def renewal_file_for_certname(config, certname): """Return /path/to/certname.conf in the renewal conf directory""" path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname)) @@ -1055,6 +1059,23 @@ def new_lineage(cls, lineagename, cert, privkey, chain, cli_config): target, values) return cls(new_config.filename, cli_config) + @property + def private_key_type(self): + """ + :returns: The type of algorithm for the private, RSA or ECDSA + :rtype: str + """ + with open(self.configuration["privkey"], "rb") as priv_key_file: + key = load_pem_private_key( + data=priv_key_file.read(), + password=None, + backend=default_backend() + ) + if isinstance(key, RSAPrivateKey): + return "RSA" + else: + return "ECDSA" + def save_successor(self, prior_version, new_cert, new_privkey, new_chain, cli_config): """Save new cert and chain as a successor of a prior version. diff --git a/certbot/certbot/crypto_util.py b/certbot/certbot/crypto_util.py index 5de412b5ec4..c8382402aff 100644 --- a/certbot/certbot/crypto_util.py +++ b/certbot/certbot/crypto_util.py @@ -11,14 +11,16 @@ import re # See https://github.com/pyca/cryptography/issues/4275 from cryptography import x509 # type: ignore -from cryptography.exceptions import InvalidSignature +from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm 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 import ec +from cryptography.hazmat.primitives.asymmetric.ec import ECDSA, EllipticCurvePublicKey from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey +from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat from OpenSSL import crypto from OpenSSL import SSL # type: ignore + import pyrfc3339 import six import zope.component @@ -34,7 +36,9 @@ # High level functions -def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): +def init_save_key( + key_size, key_dir, key_type="rsa", elliptic_curve="secp256r1", keyname="key-certbot.pem" +): """Initializes and saves a privkey. Inits key and saves it in PEM format on the filesystem. @@ -42,8 +46,10 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): .. note:: keyname is the attempted filename, it may be different if a file already exists at the path. - :param int key_size: RSA key size in bits + :param int key_size: key size in bits if key size is rsa. :param str key_dir: Key save directory. + :param str key_type: Key Type [rsa, ecdsa] + :param str elliptic_curve: Name of the elliptic curve if key type is ecdsa. :param str keyname: Filename of key :returns: Key @@ -53,7 +59,9 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): """ try: - key_pem = make_key(key_size) + key_pem = make_key( + bits=key_size, elliptic_curve=elliptic_curve or "secp256r1", key_type=key_type, + ) except ValueError as err: logger.error("", exc_info=True) raise err @@ -65,7 +73,10 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): os.path.join(key_dir, keyname), 0o600, "wb") with key_f: key_f.write(key_pem) - logger.debug("Generating key (%d bits): %s", key_size, key_path) + if key_type == 'rsa': + logger.debug("Generating RSA key (%d bits): %s", key_size, key_path) + else: + logger.debug("Generating ECDSA key (%d bits): %s", key_size, key_path) return util.Key(key_path, key_pem) @@ -174,18 +185,45 @@ def import_csr_file(csrfile, data): return PEM, util.CSR(file=csrfile, data=data_pem, form="pem"), domains -def make_key(bits): - """Generate PEM encoded RSA key. +def make_key(bits=1024, key_type="rsa", elliptic_curve=None): + """Generate PEM encoded RSA|EC key. - :param int bits: Number of bits, at least 1024. + :param int bits: Number of bits if key_type=rsa. At least 1024 for RSA. - :returns: new RSA key in PEM form with specified number of bits - :rtype: str + :param str ec_curve: The elliptic curve to use. + :returns: new RSA or ECDSA key in PEM form with specified number of bits + or of type ec_curve when key_type ecdsa is used. + :rtype: str """ - assert bits >= 1024 # XXX - key = crypto.PKey() - key.generate_key(crypto.TYPE_RSA, bits) + if key_type == 'rsa': + if bits < 1024: + raise errors.Error("Unsupported RSA key length: {}".format(bits)) + + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, bits) + elif key_type == 'ecdsa': + try: + name = elliptic_curve.upper() + if name in ('SECP256R1', 'SECP384R1', 'SECP512R1'): + _key = ec.generate_private_key( + curve=getattr(ec, elliptic_curve.upper(), None)(), + backend=default_backend() + ) + else: + raise errors.Error("Unsupported elliptic curve: {}".format(elliptic_curve)) + except TypeError: + raise errors.Error("Unsupported elliptic curve: {}".format(elliptic_curve)) + except UnsupportedAlgorithm as e: + raise six.raise_from(e, errors.Error(str(e))) + _key_pem = _key.private_bytes( + encoding=Encoding.PEM, + format=PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=NoEncryption() + ) + key = crypto.load_privatekey(crypto.FILETYPE_PEM, _key_pem) + else: + raise errors.Error("Invalid key_type specified: {}. Use [rsa|ecdsa]".format(key_type)) return crypto.dump_privatekey(crypto.FILETYPE_PEM, key) @@ -447,9 +485,8 @@ def _notAfterBefore(cert_path, method): """ # pylint: disable=redefined-outer-name - with open(cert_path) as f: - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, - f.read()) + with open(cert_path, "rb") as f: # type: IO[bytes] + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) # pyopenssl always returns bytes timestamp = method(x509) reformatted_timestamp = [timestamp[0:4], b"-", timestamp[4:6], b"-", @@ -520,6 +557,7 @@ def cert_and_chain_from_fullchain(fullchain_pem): # Since each normalized cert has a newline suffix, no extra newlines are required. return (certs_normalized[0], "".join(certs_normalized[1:])) + def get_serial_from_cert(cert_path): """Retrieve the serial number of a certificate from certificate path @@ -529,9 +567,8 @@ def get_serial_from_cert(cert_path): :rtype: int """ # pylint: disable=redefined-outer-name - with open(cert_path) as f: - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, - f.read()) + with open(cert_path, "rb") as f: # type: IO[bytes] + x509 = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) return x509.get_serial_number() diff --git a/certbot/certbot/display/util.py b/certbot/certbot/display/util.py index bc56f877804..c48454637e7 100644 --- a/certbot/certbot/display/util.py +++ b/certbot/certbot/display/util.py @@ -16,6 +16,7 @@ import zope.interface import zope.component +from acme.magic_typing import List from certbot import errors from certbot import interfaces from certbot._internal import constants @@ -105,7 +106,7 @@ def notify(msg): """ zope.component.getUtility(interfaces.IDisplay).notification( - msg, pause=False, decorate=False + msg, pause=False, decorate=False, wrap=False ) @@ -633,3 +634,28 @@ def _parens_around_char(label): """ return "({first}){rest}".format(first=label[0], rest=label[1:]) + + +def summarize_domain_list(domains): + # type: (List[str]) -> str + """Summarizes a list of domains in the format of: + example.com.com and N more domains + or if there is are only two domains: + example.com and www.example.com + or if there is only one domain: + example.com + + :param list domains: `str` list of domains + :returns: the domain list summary + :rtype: str + """ + if not domains: + return "" + + l = len(domains) + if l == 1: + return domains[0] + elif l == 2: + return " and ".join(domains) + else: + return "{0} and {1} more domains".format(domains[0], l-1) diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 43f081b9586..6ba28bd5606 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -198,6 +198,13 @@ class IConfig(zope.interface.Interface): "register multiple emails, ex: u1@example.com,u2@example.com. " "(default: Ask).") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") + elliptic_curve = zope.interface.Attribute( + "The SECG elliptic curve name to use. Please see RFC 8446 " + "for supported values." + ) + key_type = zope.interface.Attribute( + "Type of generated private key" + "(Only *ONE* per invocation can be provided at this time)") must_staple = zope.interface.Attribute( "Adds the OCSP Must Staple extension to the certificate. " "Autoconfigures OCSP Stapling for supported setups " @@ -260,6 +267,7 @@ class IConfig(zope.interface.Interface): "offered chain will be used." ) + class IInstaller(IPlugin): """Generic Certbot Installer Interface. diff --git a/certbot/certbot/tests/testdata/README b/certbot/certbot/tests/testdata/README index 8672159160a..ade2fae2b43 100644 --- a/certbot/certbot/tests/testdata/README +++ b/certbot/certbot/tests/testdata/README @@ -2,10 +2,16 @@ 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 +For the elliptic curve private keys, this command was used: + + for k in "prime256v1" "secp384r1" "secp521r1" do + openssl genpkey -algorithm ${k} -out ec_${k}_key.pem + done + and for the CSR PEM (Certificate Signing Request): openssl req -new -out csr-Xsans_X.pem -key rsa512_key.pem [-config csr-Xsans_X.conf | -subj '/CN=example.com'] [-outform DER > csr_X.der] and for the certificate: - openssl req -new -out cert_X.pem -key rsaX_key.pem -subj '/CN=example.com' -x509 [-outform DER > cert_X.der] \ No newline at end of file + openssl req -new -out cert_X.pem -key rsaX_key.pem -subj '/CN=example.com' -x509 [-outform DER > cert_X.der] diff --git a/certbot/certbot/tests/testdata/ec_prime256v1_key.pem b/certbot/certbot/tests/testdata/ec_prime256v1_key.pem new file mode 100644 index 00000000000..77552a6569d --- /dev/null +++ b/certbot/certbot/tests/testdata/ec_prime256v1_key.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDqQPQl69kuh+DrecC8SFPt21f0F/HHDP3T4/Lf0zIVFoAoGCCqGSM49 +AwEHoUQDQgAEHou50Ee9u+8Vial6VbUHExlzsiCHtORlW0X0pKo5RspIKB0QyKwo +dUXvBbv95I9yCO5+MlGkKjwLHtIEze0Hww== +-----END EC PRIVATE KEY----- diff --git a/certbot/certbot/tests/testdata/ec_secp384r1_key.pem b/certbot/certbot/tests/testdata/ec_secp384r1_key.pem new file mode 100644 index 00000000000..19a846a46fb --- /dev/null +++ b/certbot/certbot/tests/testdata/ec_secp384r1_key.pem @@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAgvZGw5C7Mp26N0cXA+vIg5K/J5MJw+MVGYfGF4ZutuCLeYMrWT68R +A0h6hJvDtMSgBwYFK4EEACKhZANiAAR1uQYZeU5Kml5o53Q8/PCdwUbqdgCSkV0C +J5a6bhDRMp20fdp2T/mbkdxuVEl81lqfKPZhsd4CZsLaVIU3RUoGgIT1R3QKawpJ +SuXq37yWFX2hqlgt+lsBufZ8RD5QnZc= +-----END EC PRIVATE KEY----- diff --git a/certbot/certbot/tests/testdata/ec_secp521r1_key.pem b/certbot/certbot/tests/testdata/ec_secp521r1_key.pem new file mode 100644 index 00000000000..78bb9a0ea30 --- /dev/null +++ b/certbot/certbot/tests/testdata/ec_secp521r1_key.pem @@ -0,0 +1,10 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIACWWVKm1qAIejZ6qmqk9D69wQW5FAe3Er0IxWAMkonTEhu8EH5Q2i +2vT2bESm730zhGTe2Pn11b85H6UI9hxhCHygBwYFK4EEACOhgYkDgYYABAEQi1WF +m3suHjPyWACyOJYGUn1Kx6rfBo0PjC7X2TU9jr8umLkIpaaF5UsBuMBmdz1IHL0U +k0gQtoOQ0Qu8N74GuAGzGR0S3RYIv6gfYVz3dS1K4n4b307Lx62bnvtlNxcIvt3w +hmS5OdvQ1Kdxh6oqbSVhhbQmJcgab78Txx3R2QeCxw== +-----END EC PRIVATE KEY----- diff --git a/certbot/certbot/tests/testdata/sample-archive-ec/cert1.pem b/certbot/certbot/tests/testdata/sample-archive-ec/cert1.pem new file mode 100644 index 00000000000..b07af2bf75c --- /dev/null +++ b/certbot/certbot/tests/testdata/sample-archive-ec/cert1.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC2zCCAcOgAwIBAgIIBvrEnbPRYu8wDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE +AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjEwNzQw +WhcNMjUxMDEyMjEwNzQwWjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs +ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARjMhuW0ENPPC33PjB5XsYU +CRw640kPQENIDatcTJaENZIZdqKd6rI6jc+lpbmXot7Zi52clJlSJS+V6oDAt2Lh +o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUj7Kd3ENqxlPf8B2bIGhsjydX +mPswHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE +JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww +GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQCl +k0JXsa8y7fg41WWMDhw60bPW77O0FtOmTcnhdI5daYNemQVk+Q5EMaBLQ/oGjgXd +9QXFzXH1PL904YEnSLt+iTpXn++7rQSNzQsdYqw0neWk4f5pEBiN+WORpb6mwobV +ifMtBOkNEHvrJ2Pkci9U1lLwtKD/DSew6QtJU5DSkmH1XdGuMJiubygEIvELtvgq +cP9S368ZvPmPGmKaJQXBiuaR8MTjY/Bkr79aXQMjKbf+mpn7h0POCcePk1DY/rm6 +Da+X16lf0hHyQhSUa7Vgyim6rK1/hlw+Z00i+sQCKD9Ih7kXuuGqfSDC33cfO8Tj +o/MXO8lcxkrem5zU5QWP +-----END CERTIFICATE----- diff --git a/certbot/certbot/tests/testdata/sample-archive-ec/chain1.pem b/certbot/certbot/tests/testdata/sample-archive-ec/chain1.pem new file mode 100644 index 00000000000..64a805c0fd1 --- /dev/null +++ b/certbot/certbot/tests/testdata/sample-archive-ec/chain1.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx +MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy +NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq +mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB +qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5 +CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH +nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY +MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx +PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE +bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT +MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq +uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P +fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV +EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW +fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG +9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE +-----END CERTIFICATE----- diff --git a/certbot/certbot/tests/testdata/sample-archive-ec/fullchain1.pem b/certbot/certbot/tests/testdata/sample-archive-ec/fullchain1.pem new file mode 100644 index 00000000000..1ba80ba4e9e --- /dev/null +++ b/certbot/certbot/tests/testdata/sample-archive-ec/fullchain1.pem @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIC2zCCAcOgAwIBAgIILlmGtZhUFEwwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE +AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjA1MDM0 +WhcNMjUxMDEyMjA1MDM0WjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs +ZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARHEzR8JPWrEmpmgM+F2bk5 +9mT0u6CjzmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/ +o4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU1CsVL+bPnzaxxQ5jUENmQJIO +lKwwHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE +JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww +GoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBn +2D8loC7pfk28JYpFLr5lmFKJWWmtLGlpsWDj61fVjtTfGKLziJz+MM6il4Y3hIz5 +58qiFK0ue0M63dIBJ33N+XxSEXon4Q0gy/zRWfH9jtPJ3FwfjkU/RT9PAUClYi0G +ptNWnTmgQkNzousbcAtRNXuuShH3856vhUnwkX+xM+cbIDi1JVmFjcGrEEQJ0rUF +mv2ZTyfbWbUs3v4rReETi2NVzr1Ql6J+ByNcMvHODzFy3t0L6yelAw2ca1I+c9HU ++Z0tnp/ykR7eXNuVLivok8UBf5OC413lh8ZO5g+Bgzh/LdtkUuavg1MYtEX0H6mX +9U7y3nVI8WEbPGf+HDeu +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx +MDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy +NmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq +mJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB +qKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5 +CIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH +nm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY +MRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx +PzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE +bIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT +MA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq +uCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P +fJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV +EdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW +fcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG +9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE +-----END CERTIFICATE----- diff --git a/certbot/certbot/tests/testdata/sample-archive-ec/privkey1.pem b/certbot/certbot/tests/testdata/sample-archive-ec/privkey1.pem new file mode 100644 index 00000000000..6843b83d6e4 --- /dev/null +++ b/certbot/certbot/tests/testdata/sample-archive-ec/privkey1.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNgefv2dad4U1VYEi +0WkdHuqywi5QXAe30OwNTTGjhbihRANCAARHEzR8JPWrEmpmgM+F2bk59mT0u6Cj +zmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/ +-----END PRIVATE KEY----- diff --git a/certbot/certbot/tests/testdata/sample-renewal-ec.conf b/certbot/certbot/tests/testdata/sample-renewal-ec.conf new file mode 100644 index 00000000000..d8a4b32bff0 --- /dev/null +++ b/certbot/certbot/tests/testdata/sample-renewal-ec.conf @@ -0,0 +1,79 @@ +# add some stuff here +# assets/integration_tests + +cert = MAGICDIR/live/sample-renewal-ec/cert.pem +privkey = MAGICDIR/live/sample-renewal-ec/privkey.pem +chain = MAGICDIR/live/sample-renewal-ec/chain.pem +fullchain = MAGICDIR/live/sample-renewal-ec/fullchain.pem +renew_before_expiry = 4 years + +# Options and defaults used in the renewal process +[renewalparams] +no_self_upgrade = False +apache_enmod = a2enmod +no_verify_ssl = False +ifaces = None +apache_dismod = a2dismod +register_unsafely_without_email = False +apache_handle_modules = True +uir = None +installer = None +nginx_ctl = nginx +config_dir = MAGICDIR +text_mode = False +func = +staging = True +prepare = False +work_dir = /var/lib/letsencrypt +tos = False +init = False +http01_port = 80 +duplicate = False +noninteractive_mode = True +key_path = None +nginx = False +nginx_server_root = /etc/nginx +fullchain_path = /home/ubuntu/letsencrypt/chain.pem +email = None +csr = None +agree_dev_preview = None +redirect = None +verb = certonly +verbose_count = -3 +config_file = None +renew_by_default = False +hsts = False +apache_handle_sites = True +authenticator = standalone +domains = isnot.org, +key_type = ecdsa +elliptic_curve = secp256r1 +apache_challenge_location = /etc/apache2 +checkpoints = 1 +manual_test_mode = False +apache = False +cert_path = /home/ubuntu/letsencrypt/cert.pem +webroot_path = None +reinstall = False +expand = False +strict_permissions = False +apache_server_root = /etc/apache2 +account = None +dry_run = False +manual_public_ip_logging_ok = False +chain_path = /home/ubuntu/letsencrypt/chain.pem +break_my_certs = False +standalone = True +manual = False +server = https://acme-staging-v02.api.letsencrypt.org/directory +webroot = False +os_packages_only = False +apache_init_script = None +user_agent = None +apache_le_vhost_ext = -le-ssl.conf +debug = False +logs_dir = /var/log/letsencrypt +apache_vhost_root = /etc/apache2/sites-available +configurator = None +must_staple = True +[[webroot_map]] diff --git a/certbot/certbot/tests/testdata/sample-renewal.conf b/certbot/certbot/tests/testdata/sample-renewal.conf index 936c5c0e0df..0c56781b3dd 100644 --- a/certbot/certbot/tests/testdata/sample-renewal.conf +++ b/certbot/certbot/tests/testdata/sample-renewal.conf @@ -44,6 +44,7 @@ apache_handle_sites = True authenticator = standalone domains = isnot.org, rsa_key_size = 2048 +elliptic_curve = secp256r1 apache_challenge_location = /etc/apache2 checkpoints = 1 manual_test_mode = False diff --git a/certbot/certbot/tests/util.py b/certbot/certbot/tests/util.py index 92f52a85204..b9d5caa08b3 100644 --- a/certbot/certbot/tests/util.py +++ b/certbot/certbot/tests/util.py @@ -93,7 +93,7 @@ def load_pyopenssl_private_key(*names): return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) -def make_lineage(config_dir, testfile): +def make_lineage(config_dir, testfile, ec=False): """Creates a lineage defined by testfile. This creates the archive, live, and renewal directories if @@ -119,7 +119,7 @@ def make_lineage(config_dir, testfile): if not os.path.exists(directory): filesystem.makedirs(directory) - sample_archive = vector_path('sample-archive') + sample_archive = vector_path('sample-archive{}'.format('-ec' if ec else '')) for kind in os.listdir(sample_archive): shutil.copyfile(os.path.join(sample_archive, kind), os.path.join(archive_dir, kind)) diff --git a/certbot/certbot/util.py b/certbot/certbot/util.py index e1249010104..ab28b4159ca 100644 --- a/certbot/certbot/util.py +++ b/certbot/certbot/util.py @@ -439,7 +439,7 @@ def safe_email(email): return False -class _ShowWarning(argparse.Action): +class DeprecatedArgumentAction(argparse.Action): """Action to log a warning when an argument is used.""" def __call__(self, unused1, unused2, unused3, option_string=None): logger.warning("Use of %s is deprecated.", option_string) @@ -458,16 +458,16 @@ def add_deprecated_argument(add_argument, argument_name, nargs): :param nargs: Value for nargs when adding the argument to argparse. """ - if _ShowWarning not in configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE: + if DeprecatedArgumentAction not in configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE: # In version 0.12.0 ACTION_TYPES_THAT_DONT_NEED_A_VALUE was # changed from a set to a tuple. if isinstance(configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE, set): configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add( - _ShowWarning) + DeprecatedArgumentAction) else: configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE += ( - _ShowWarning,) - add_argument(argument_name, action=_ShowWarning, + DeprecatedArgumentAction,) + add_argument(argument_name, action=DeprecatedArgumentAction, help=argparse.SUPPRESS, nargs=nargs) diff --git a/certbot/docs/ciphers.rst b/certbot/docs/ciphers.rst index 294e6a7fa1d..43f64889829 100644 --- a/certbot/docs/ciphers.rst +++ b/certbot/docs/ciphers.rst @@ -80,8 +80,8 @@ its preferences in accordance with its own policy or its administrators' preferences, and use different cryptographic mechanisms or parameters, or a different priority order, than the defaults provided by Certbot. -If you don't use Certbot to configure your server directly, because the -client doesn't integrate with your server software or because you chose +If you don't use Certbot to configure your server directly, because the +client doesn't integrate with your server software or because you chose not to use this integration, then the cryptographic defaults haven't been modified, and the cryptography chosen by the server will still be whatever the default for your software was. For example, if you obtain a @@ -254,7 +254,7 @@ https://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elb-secur U.S. Government 18F ~~~~~~~~~~~~~~~~~~~ -The 18F site (https://18f.gsa.gov/) is using +The 18F site (https://18f.gsa.gov/) is using :: diff --git a/certbot/docs/cli-help.txt b/certbot/docs/cli-help.txt index 3c3497282a7..a320b30e8bf 100644 --- a/certbot/docs/cli-help.txt +++ b/certbot/docs/cli-help.txt @@ -118,12 +118,12 @@ optional arguments: case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to - "". (default: CertbotACMEClient/1.9.0 (certbot(-auto); - OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY - (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel). - The flags encoded in the user agent are: --duplicate, - --force-renew, --allow-subset-of-names, -n, and - whether any hooks are set. + "". (default: CertbotACMEClient/1.10.1 + (certbot(-auto); OS_NAME OS_VERSION) Authenticator/XXX + Installer/YYY (SUBCOMMAND; flags: FLAGS) + Py/major.minor.patchlevel). The flags encoded in the + user agent are: --duplicate, --force-renew, --allow- + subset-of-names, -n, and whether any hooks are set. --user-agent-comment USER_AGENT_COMMENT Add a comment to the default user agent string. May be used when repackaging Certbot or calling it from @@ -188,6 +188,12 @@ security: Security parameters & server settings --rsa-key-size N Size of the RSA key. (default: 2048) + --key-type {rsa,ecdsa} + Type of generated private key(Only *ONE* per + invocation can be provided at this time) (default: + rsa) + --elliptic-curve N The SECG elliptic curve name to use. Please see RFC + 8446 for supported values. (default: secp256r1) --must-staple Adds the OCSP Must Staple extension to the certificate. Autoconfigures OCSP Stapling for supported setups (Apache version >= 2.3.3 ). (default: @@ -688,8 +694,6 @@ manual: --manual-cleanup-hook MANUAL_CLEANUP_HOOK Path or command to execute for the cleanup script (default: None) - --manual-public-ip-logging-ok - Automatically allows public IP logging (default: Ask) nginx: Nginx Web Server plugin diff --git a/certbot/docs/compatibility.rst b/certbot/docs/compatibility.rst index a511f36a21f..a4f33c281b8 100644 --- a/certbot/docs/compatibility.rst +++ b/certbot/docs/compatibility.rst @@ -9,7 +9,7 @@ application itself. This means that we will not change behavior in a backwards incompatible way except in a new major version of the project. .. note:: None of this applies to the behavior of Certbot distribution - mechanisms such as :ref:`certbot-auto ` or OS packages whose + mechanisms such as :ref:`our snaps ` or OS packages whose behavior may change at any time. Semantic versioning only applies to the common Certbot components that are installed by various distribution methods. diff --git a/certbot/docs/contributing.rst b/certbot/docs/contributing.rst index 32a1ee72b5e..4e2643d7c21 100644 --- a/certbot/docs/contributing.rst +++ b/certbot/docs/contributing.rst @@ -169,7 +169,7 @@ To do so you need: - Docker installed, and a user with access to the Docker client, - an available `local copy`_ of Certbot. -The virtual environment set up with `python tools/venv3.py` contains two commands +The virtual environment set up with `python tools/venv3.py` contains two CLI tools that can be used once the virtual environment is activated: .. code-block:: shell @@ -180,6 +180,9 @@ that can be used once the virtual environment is activated: - Press CTRL+C to stop this instance. - This instance is configured to validate challenges against certbot executed locally. +.. note:: Some options are available to tweak the local ACME server. You can execute + ``run_acme_server --help`` to see the inline help of the ``run_acme_server`` tool. + .. code-block:: shell certbot_test [ARGS...] @@ -431,16 +434,23 @@ Please: 4. Remember to use ``pylint``. +5. You may consider installing a plugin for `editorconfig`_ in + your editor to prevent some linting warnings. + +6. Please avoid `unittest.assertTrue` or `unittest.assertFalse` when + possible, and use `assertEqual` or more specific assert. They give + better messages when it's failing, and are generally more correct. + .. _Google Python Style Guide: https://google.github.io/styleguide/pyguide.html .. _Sphinx-style: https://www.sphinx-doc.org/ .. _PEP 8 - Style Guide for Python Code: https://www.python.org/dev/peps/pep-0008 +.. _editorconfig: https://editorconfig.org/ Use ``certbot.compat.os`` instead of ``os`` =========================================== - Python's standard library ``os`` module lacks full support for several Windows security features about file permissions (eg. DACLs). However several files handled by Certbot (eg. private keys) need strongly restricted access @@ -506,11 +516,13 @@ Steps: 4. Run ``tox --skip-missing-interpreters`` to run the entire test suite including coverage. The ``--skip-missing-interpreters`` argument ignores missing versions of Python needed for running the tests. Fix any errors. -5. Submit the PR. Once your PR is open, please do not force push to the branch +5. If any documentation should be added or updated as part of the changes you + have made, please include the documentation changes in your PR. +6. Submit the PR. Once your PR is open, please do not force push to the branch containing your pull request to squash or amend commits. We use `squash merges `_ on PRs and rewriting commits makes changes harder to track between reviews. -6. Did your tests pass on Azure Pipelines? If they didn't, fix any errors. +7. Did your tests pass on Azure Pipelines? If they didn't, fix any errors. .. _ask for help: diff --git a/certbot/docs/install.rst b/certbot/docs/install.rst index c1d8cc40310..df32bb60e8f 100644 --- a/certbot/docs/install.rst +++ b/certbot/docs/install.rst @@ -44,17 +44,6 @@ supports `_ modern OSes based on Debian, Ubuntu, Fedora, SUSE, Gentoo and Darwin. - -Additional integrity verification of certbot-auto script can be done by verifying its digital signature. -This requires a local installation of gpg2, which comes packaged in many Linux distributions under name gnupg or gnupg2. - - -Installing with ``certbot-auto`` requires 512MB of RAM in order to build some -of the dependencies. Installing from pre-built OS packages avoids this -requirement. You can also temporarily set a swap file. See "Problems with -Python virtual environment" below for details. - - Alternate installation methods ================================ @@ -78,74 +67,6 @@ choosing "snapd" in the "System" dropdown menu. (You should select "snapd" regardless of your operating system, as our instructions are the same across all systems.) -.. _certbot-auto: - -Certbot-Auto ------------- - -The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies -from your web server OS and putting others in a python virtual environment. You can -download and run it as follows:: - - wget https://dl.eff.org/certbot-auto - sudo mv certbot-auto /usr/local/bin/certbot-auto - sudo chown root /usr/local/bin/certbot-auto - sudo chmod 0755 /usr/local/bin/certbot-auto - /usr/local/bin/certbot-auto --help - -To remove certbot-auto, just delete it and the files it places under /opt/eff.org, along with any cronjob or systemd timer you may have created. - -To check the integrity of the ``certbot-auto`` script, -you can use these steps:: - - - user@webserver:~$ wget -N https://dl.eff.org/certbot-auto.asc - user@webserver:~$ gpg2 --keyserver pool.sks-keyservers.net --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 - user@webserver:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.asc /usr/local/bin/certbot-auto - - - -The output of the last command should look something like:: - - - gpg: Signature made Wed 02 May 2018 05:29:12 AM IST - gpg: using RSA key A2CFB51FA275A7286234E7B24D17C995CD9775F2 - gpg: key 4D17C995CD9775F2 marked as ultimately trusted - gpg: checking the trustdb - gpg: marginals needed: 3 completes needed: 1 trust model: pgp - gpg: depth: 0 valid: 2 signed: 2 trust: 0-, 0q, 0n, 0m, 0f, 2u - gpg: depth: 1 valid: 2 signed: 0 trust: 2-, 0q, 0n, 0m, 0f, 0u - gpg: next trustdb check due at 2027-11-22 - gpg: Good signature from "Let's Encrypt Client Team " [ultimate] - - - -The ``certbot-auto`` command updates to the latest client release automatically. -Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly -the same command line flags and arguments. For more information, see -`Certbot command-line options `_. - -For full command line help, you can type:: - - /usr/local/bin/certbot-auto --help all - -Problems with Python virtual environment ----------------------------------------- - -On a low memory system such as VPS with less than 512MB of RAM, the required dependencies of Certbot will fail to build. -This can be identified if the pip outputs contains something like ``internal compiler error: Killed (program cc1)``. -You can workaround this restriction by creating a temporary swapfile:: - - user@webserver:~$ sudo fallocate -l 1G /tmp/swapfile - user@webserver:~$ sudo chmod 600 /tmp/swapfile - user@webserver:~$ sudo mkswap /tmp/swapfile - user@webserver:~$ sudo swapon /tmp/swapfile - -Disable and remove the swapfile once the virtual environment is constructed:: - - user@webserver:~$ sudo swapoff /tmp/swapfile - user@webserver:~$ sudo rm /tmp/swapfile - .. _docker-user: Running with Docker @@ -207,6 +128,18 @@ of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. Operating System Packages ------------------------- +.. warning:: While the Certbot team tries to keep the Certbot packages offered + by various operating systems working in the most basic sense, due to + distribution policies and/or the limited resources of distribution + maintainers, Certbot OS packages often have problems that other distribution + mechanisms do not. The packages are often old resulting in a lack of bug + fixes and features and a worse TLS configuration than is generated by newer + versions of Certbot. They also may not configure certificate renewal for you + or have all of Certbot's plugins available. For reasons like these, we + recommend most users follow the instructions at + https://certbot.eff.org/instructions and OS packages are only documented + here as an alternative. + **Arch Linux** .. code-block:: shell @@ -303,6 +236,33 @@ OS packaging is an ongoing effort. If you'd like to package Certbot for your distribution of choice please have a look at the :doc:`packaging`. +.. _certbot-auto: + +Certbot-Auto +------------ + +We used to have a shell script named ``certbot-auto`` to help people install +Certbot on UNIX operating systems, however, this script is no longer supported. + +Problems with Python virtual environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using ``certbot-auto`` on a low memory system such as VPS with less than +512MB of RAM, the required dependencies of Certbot may fail to build. This can +be identified if the pip outputs contains something like ``internal compiler +error: Killed (program cc1)``. You can workaround this restriction by creating +a temporary swapfile:: + + user@webserver:~$ sudo fallocate -l 1G /tmp/swapfile + user@webserver:~$ sudo chmod 600 /tmp/swapfile + user@webserver:~$ sudo mkswap /tmp/swapfile + user@webserver:~$ sudo swapon /tmp/swapfile + +Disable and remove the swapfile once the virtual environment is constructed:: + + user@webserver:~$ sudo swapoff /tmp/swapfile + user@webserver:~$ sudo rm /tmp/swapfile + Installing from source ---------------------- diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index 290ac817a59..50f5b13fd77 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -179,10 +179,9 @@ If you'd like to obtain a wildcard certificate from Let's Encrypt or run Certbot's DNS plugins. These plugins are not included in a default Certbot installation and must be -installed separately. While the DNS plugins cannot currently be used with -``certbot-auto``, they are available in many OS package managers, as Docker -images, and as snaps. Visit https://certbot.eff.org to learn the best way to use -the DNS plugins on your system. +installed separately. They are available in many OS package managers, as Docker +images, and as snaps. Visit https://certbot.eff.org to learn the best way to +use the DNS plugins on your system. Once installed, you can find documentation on how to use each plugin at: @@ -319,6 +318,7 @@ This returns information in the following format:: Domains: example.com, www.example.com Expiry Date: 2017-02-19 19:53:00+00:00 (VALID: 30 days) Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem + Key Type: RSA Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem ``Certificate Name`` shows the name of the certificate. Pass this name @@ -346,7 +346,6 @@ control Certbot's behavior when re-creating a certificate with the same name as an existing certificate. If you don't specify a requested behavior, Certbot may ask you what you intended. - ``--force-renewal`` tells Certbot to request a new certificate with the same domains as an existing certificate. Each domain must be explicitly specified via ``-d``. If successful, this certificate @@ -380,7 +379,6 @@ If you prefer, you can specify the domains individually like this: Consider using ``--cert-name`` instead of ``--expand``, as it gives more control over which certificate is modified and it lets you remove domains as well as adding them. - ``--allow-subset-of-names`` tells Certbot to continue with certificate generation if only some of the specified domain authorizations can be obtained. This may be useful if some domains specified in a certificate no longer point at this @@ -411,6 +409,68 @@ replace that set entirely:: certbot certonly --cert-name example.com -d example.org,www.example.org +Using ECDSA keys +---------------- + +As of version 1.10, Certbot supports two types of private key algorithms: +``rsa`` and ``ecdsa``. The type of key used by Certbot can be controlled +through the ``--key-type`` option. You can also use the ``--elliptic-curve`` +option to control the curve used in ECDSA certificates. + +.. warning:: If you obtain certificates using ECDSA keys, you should be careful + not to downgrade your Certbot installation since ECDSA keys are not + supported by older versions of Certbot. Downgrades like this are possible if + you switch from something like the snaps or certbot-auto to packages + provided by your operating system which often lag behind. + +Changing existing certificates from RSA to ECDSA +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unless you are aware that you need to support very old HTTPS clients that are +not supported by most sites, you can safely just transition your site to use +ECDSA keys instead of RSA keys. To accomplish this if you have existing +certificates managed by Certbot, you may freely change the certificate to a new +private key. + +If you want to use ECDSA keys for all certificates in the future, you can +simply add the following line to Certbot's :ref:`configuration file ` + +.. code-block:: ini + + key-type = ecdsa + +After this option is set, newly obtained certificates will use ECDSA keys. This +includes certificates managed by Certbot that previously used RSA keys. + +If you want to change a single certificate to use ECDSA keys, you'll need to +issue a new Certbot command setting ``--key-type ecdsa`` on the command line +like + +.. code-block:: shell + + certbot renew --key-type ecdsa --cert-name example.com --force-renewal + +Obtaining ECDSA certificates in addition to RSA certificates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When Certbot configures the certificates it obtains with Apache or Nginx, all +HTTPS clients that we try to support can use certificates with ECDSA keys. If, +however, you are aware of having a specific need to support very old TLS +clients, you may want to obtain both ECDSA and RSA certificates for the same +domains. Certbot can only configure Apache or Nginx to use a single +certificate, however, you could manually configure your software to use the +different certificates depending on your needs. + +When obtaining both ECDSA and RSA certificates for the same domains with +Certbot, we recommend using the ``--cert-name`` option to give your +certificates names so that you can easily identify them. For instance, you may +want to append "ecdsa" to the name of your ECDSA certificate by using a command +like + +.. code-block:: shell + + certbot certonly --key-type ecdsa --cert-name example.com-ecdsa + Revoking certificates --------------------- diff --git a/certbot/examples/cli.ini b/certbot/examples/cli.ini index 4215fda5b5f..dfb1d6fffa3 100644 --- a/certbot/examples/cli.ini +++ b/certbot/examples/cli.ini @@ -7,6 +7,10 @@ # certificate on a system with several certificates should not be placed # here. +# Use ECC for the private key +key-type = ecdsa +elliptic-curve = secp384r1 + # Use a 4096 bit RSA key instead of 2048 rsa-key-size = 4096 diff --git a/certbot/setup.py b/certbot/setup.py index 742900402b5..ea87d230187 100644 --- a/certbot/setup.py +++ b/certbot/setup.py @@ -131,6 +131,7 @@ def read_file(filename, encoding='utf8'): 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 6cd207b4bd9..8eb5d770224 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -510,7 +510,7 @@ def test_same_error_and_domain(self, mock_zope): auth_handler._report_failed_authzrs([self.authzr1], 'key') call_list = mock_zope().add_message.call_args_list - self.assertTrue(len(call_list) == 1) + self.assertEqual(len(call_list), 1) self.assertTrue("Domain: example.com\nType: tls\nDetail: detail" in call_list[0][0][0]) @test_util.patch_get_utility() @@ -518,7 +518,7 @@ def test_different_errors_and_domains(self, mock_zope): from certbot._internal import auth_handler auth_handler._report_failed_authzrs([self.authzr1, self.authzr2], 'key') - self.assertTrue(mock_zope().add_message.call_count == 2) + self.assertEqual(mock_zope().add_message.call_count, 2) def gen_auth_resp(chall_list): diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 7475e99eaa0..e65ec32ec73 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -359,6 +359,21 @@ def test_option_was_set(self): self.assertFalse(cli.option_was_set( 'authenticator', cli.flag_default('authenticator'))) + def test_ecdsa_key_option(self): + elliptic_curve_option = 'elliptic_curve' + elliptic_curve_option_value = cli.flag_default(elliptic_curve_option) + self.parse('--elliptic-curve {0}'.format(elliptic_curve_option_value).split()) + self.assertIs(cli.option_was_set(elliptic_curve_option, elliptic_curve_option_value), True) + + def test_invalid_key_type(self): + key_type_option = 'key_type' + key_type_value = cli.flag_default(key_type_option) + self.parse('--key-type {0}'.format(key_type_value).split()) + self.assertIs(cli.option_was_set(key_type_option, key_type_value), True) + + with self.assertRaises(SystemExit): + self.parse("--key-type foo") + def test_encode_revocation_reason(self): for reason, code in constants.REVOCATION_REASONS.items(): namespace = self.parse(['--reason', reason]) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 1b2a7413ea6..f058cb6583f 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -13,7 +13,6 @@ from certbot import errors from certbot import util from certbot._internal import account -from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util @@ -329,7 +328,11 @@ def test_obtain_certificate(self, mock_crypto_util): self._test_obtain_certificate_common(mock.sentinel.key, csr) mock_crypto_util.init_save_key.assert_called_once_with( - self.config.rsa_key_size, self.config.key_dir) + key_size=self.config.rsa_key_size, + key_dir=self.config.key_dir, + key_type=self.config.key_type, + elliptic_curve=None, # elliptic curve is not set + ) mock_crypto_util.init_save_csr.assert_called_once_with( mock.sentinel.key, self.eg_domains, self.config.csr_dir) mock_crypto_util.cert_and_chain_from_fullchain.assert_called_once_with( @@ -365,7 +368,11 @@ def test_obtain_certificate_dry_run(self, mock_acme_crypto, mock_crypto): self.client.config.dry_run = True self._test_obtain_certificate_common(key, csr) - mock_crypto.make_key.assert_called_once_with(self.config.rsa_key_size) + mock_crypto.make_key.assert_called_once_with( + bits=self.config.rsa_key_size, + elliptic_curve=None, # not making an elliptic private key + key_type=self.config.key_type, + ) mock_acme_crypto.make_csr.assert_called_once_with( mock.sentinel.key_pem, self.eg_domains, self.config.must_staple) mock_crypto.init_save_key.assert_not_called() diff --git a/certbot/tests/compat/filesystem_test.py b/certbot/tests/compat/filesystem_test.py index 262fd02b5f0..263029cb0e2 100644 --- a/certbot/tests/compat/filesystem_test.py +++ b/certbot/tests/compat/filesystem_test.py @@ -1,7 +1,6 @@ """Tests for certbot.compat.filesystem""" import contextlib import errno -import stat import unittest try: diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index bbd484c917f..37673db9900 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -4,7 +4,7 @@ try: import mock -except ImportError: # pragma: no cover +except ImportError: # pragma: no cover from unittest import mock import OpenSSL import zope.component @@ -32,6 +32,7 @@ CERT_ISSUER = test_util.load_vector('cert_intermediate_1.pem') CERT_ALT_ISSUER = test_util.load_vector('cert_intermediate_2.pem') + class InitSaveKeyTest(test_util.TempDirTestCase): """Tests for certbot.crypto_util.init_save_key.""" def setUp(self): @@ -174,11 +175,54 @@ def test_bad_csr(self): class MakeKeyTest(unittest.TestCase): """Tests for certbot.crypto_util.make_key.""" - def test_it(self): # pylint: disable=no-self-use + def test_rsa(self): # pylint: disable=no-self-use + # RSA Key Type Test + from certbot.crypto_util import make_key + # Do not test larger keys as it takes too long. + OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, make_key(1024)) + + def test_ec(self): # pylint: disable=no-self-use + # ECDSA Key Type Tests from certbot.crypto_util import make_key # Do not test larger keys as it takes too long. + + # Try a good key size for ECDSA OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, make_key(1024)) + OpenSSL.crypto.FILETYPE_PEM, make_key(elliptic_curve="secp256r1", key_type='ecdsa')) + + def test_bad_key_sizes(self): + from certbot.crypto_util import make_key + # Try a bad key size for RSA and ECDSA + with self.assertRaises(errors.Error) as e: + make_key(bits=512, key_type='rsa') + self.assertEqual( + "Unsupported RSA key length: 512", + str(e.exception), + "Unsupported RSA key length: 512" + ) + + def test_bad_elliptic_curve_name(self): + from certbot.crypto_util import make_key + with self.assertRaises(errors.Error) as e: + make_key(elliptic_curve="nothere", key_type='ecdsa') + self.assertEqual( + "Unsupported elliptic curve: nothere", + str(e.exception), + "Unsupported elliptic curve: nothere" + ) + + def test_bad_key_type(self): + from certbot.crypto_util import make_key + + # Try a bad --key-type + with self.assertRaises(errors.Error) as e: + OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, make_key(1024, key_type='unf')) + self.assertEqual( + "Invalid key_type specified: unf. Use [rsa|ecdsa]", + str(e.exception), + "Invalid key_type specified: unf. Use [rsa|ecdsa]", + ) class VerifyCertSetup(unittest.TestCase): diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index cdbcf19372d..ce60a5f0be1 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -43,7 +43,7 @@ def test_ok_safe(self, mock_get_utility): mock_input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.return_value = True - self.assertTrue(self._call() == "foo@bar.baz") + self.assertEqual(self._call(), "foo@bar.baz") @test_util.patch_get_utility("certbot.display.ops.z_util") def test_ok_not_safe(self, mock_get_utility): @@ -51,7 +51,7 @@ def test_ok_not_safe(self, mock_get_utility): mock_input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] - self.assertTrue(self._call() == "foo@bar.baz") + self.assertEqual(self._call(), "foo@bar.baz") @test_util.patch_get_utility("certbot.display.ops.z_util") def test_invalid_flag(self, mock_get_utility): diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index 7af454abd63..1b22d3422d5 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -454,6 +454,27 @@ def test_multiple(self): self.assertEqual("(y)es please", self._call("yes please")) +class SummarizeDomainListTest(unittest.TestCase): + @classmethod + def _call(cls, domains): + from certbot.display.util import summarize_domain_list + return summarize_domain_list(domains) + + def test_single_domain(self): + self.assertEqual("example.com", self._call(["example.com"])) + + def test_two_domains(self): + self.assertEqual("example.com and example.org", + self._call(["example.com", "example.org"])) + + def test_many_domains(self): + self.assertEqual("example.com and 2 more domains", + self._call(["example.com", "example.org", "a.example.com"])) + + def test_empty_domains(self): + self.assertEqual("", self._call([])) + + class NotifyTest(unittest.TestCase): """Test the notify function """ @@ -462,7 +483,7 @@ def test_notify(self, mock_util): from certbot.display.util import notify notify("Hello World") mock_util().notification.assert_called_with( - "Hello World", pause=False, decorate=False + "Hello World", pause=False, decorate=False, wrap=False ) diff --git a/certbot/tests/helpful_test.py b/certbot/tests/helpful_test.py index 292e5530440..1a5c2bea6c6 100644 --- a/certbot/tests/helpful_test.py +++ b/certbot/tests/helpful_test.py @@ -1,6 +1,11 @@ """Tests for certbot.helpful_parser""" import unittest +try: + import mock +except ImportError: # pragma: no cover + from unittest import mock + from certbot import errors from certbot._internal.cli import HelpfulArgumentParser from certbot._internal.cli import _DomainsAction @@ -189,5 +194,16 @@ def test_parse_args_hosts_and_auto_hosts(self): arg_parser.parse_args() +class TestAddDeprecatedArgument(unittest.TestCase): + """Tests for add_deprecated_argument method of HelpfulArgumentParser""" + + @mock.patch.object(HelpfulArgumentParser, "modify_kwargs_for_default_detection") + def test_no_default_detection_modifications(self, mock_modify): + arg_parser = HelpfulArgumentParser(["run"], {}, detect_defaults=True) + arg_parser.add_deprecated_argument("--foo", 0) + arg_parser.parse_args() + mock_modify.assert_not_called() + + if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index e17fe9a8741..1d568448eac 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -49,14 +49,51 @@ SS_CERT_PATH = test_util.vector_path('cert_2048.pem') -class TestHandleIdenticalCerts(unittest.TestCase): - """Test for certbot._internal.main._handle_identical_cert_request""" - def test_handle_identical_cert_request_pending(self): +class TestHandleCerts(unittest.TestCase): + """Test for certbot._internal.main._handle_* methods""" + @mock.patch("certbot._internal.main._handle_unexpected_key_type_migration") + def test_handle_identical_cert_request_pending(self, mock_handle_migration): mock_lineage = mock.Mock() mock_lineage.ensure_deployed.return_value = False # pylint: disable=protected-access ret = main._handle_identical_cert_request(mock.Mock(), mock_lineage) self.assertEqual(ret, ("reinstall", mock_lineage)) + self.assertTrue(mock_handle_migration.called) + + @mock.patch("certbot._internal.main._handle_unexpected_key_type_migration") + def test_handle_subset_cert_request(self, mock_handle_migration): + mock_config = mock.Mock() + mock_config.expand = True + mock_lineage = mock.Mock() + mock_lineage.names.return_value = ["dummy1", "dummy2"] + ret = main._handle_subset_cert_request(mock_config, ["dummy1"], mock_lineage) + self.assertEqual(ret, ("renew", mock_lineage)) + self.assertTrue(mock_handle_migration.called) + + @mock.patch("certbot._internal.main.cli.set_by_cli") + def test_handle_unexpected_key_type_migration(self, mock_set): + config = mock.Mock() + config.key_type = "rsa" + cert = mock.Mock() + cert.private_key_type = "ecdsa" + + mock_set.return_value = True + main._handle_unexpected_key_type_migration(config, cert) + + mock_set.return_value = False + with self.assertRaises(errors.Error) as raised: + main._handle_unexpected_key_type_migration(config, cert) + self.assertTrue("Please provide both --cert-name and --key-type" in str(raised.exception)) + + mock_set.side_effect = lambda var: var != "certname" + with self.assertRaises(errors.Error) as raised: + main._handle_unexpected_key_type_migration(config, cert) + self.assertTrue("Please provide both --cert-name and --key-type" in str(raised.exception)) + + mock_set.side_effect = lambda var: var != "key_type" + with self.assertRaises(errors.Error) as raised: + main._handle_unexpected_key_type_migration(config, cert) + self.assertTrue("Please provide both --cert-name and --key-type" in str(raised.exception)) class RunTest(test_util.ConfigTestCase): @@ -163,31 +200,35 @@ def _assert_no_pause(self, message, pause=True): # pylint: disable=unused-argum @mock.patch('certbot._internal.cert_manager.lineage_for_certname') @mock.patch('certbot._internal.cert_manager.domains_for_certname') @mock.patch('certbot._internal.renewal.renew_cert') + @mock.patch('certbot._internal.main._handle_unexpected_key_type_migration') @mock.patch('certbot._internal.main._report_new_cert') def test_find_lineage_for_domains_and_certname(self, mock_report_cert, - mock_renew_cert, mock_domains, mock_lineage): + mock_handle_type, mock_renew_cert, mock_domains, mock_lineage): domains = ['example.com', 'test.org'] mock_domains.return_value = domains mock_lineage.names.return_value = domains self._call(('certonly --webroot -d example.com -d test.org ' '--cert-name example.com').split()) - self.assertTrue(mock_lineage.call_count == 1) - self.assertTrue(mock_domains.call_count == 1) - self.assertTrue(mock_renew_cert.call_count == 1) - self.assertTrue(mock_report_cert.call_count == 1) + + self.assertEqual(mock_lineage.call_count, 1) + self.assertEqual(mock_domains.call_count, 1) + self.assertEqual(mock_renew_cert.call_count, 1) + self.assertEqual(mock_report_cert.call_count, 1) + self.assertEqual(mock_handle_type.call_count, 1) # user confirms updating lineage with new domains self._call(('certonly --webroot -d example.com -d test.com ' '--cert-name example.com').split()) - self.assertTrue(mock_lineage.call_count == 2) - self.assertTrue(mock_domains.call_count == 2) - self.assertTrue(mock_renew_cert.call_count == 2) - self.assertTrue(mock_report_cert.call_count == 2) + self.assertEqual(mock_lineage.call_count, 2) + self.assertEqual(mock_domains.call_count, 2) + self.assertEqual(mock_renew_cert.call_count, 2) + self.assertEqual(mock_report_cert.call_count, 2) + self.assertEqual(mock_handle_type.call_count, 2) # error in _ask_user_to_confirm_new_names self.mock_get_utility().yesno.return_value = False self.assertRaises(errors.ConfigurationError, self._call, - ('certonly --webroot -d example.com -d test.com --cert-name example.com').split()) + 'certonly --webroot -d example.com -d test.com --cert-name example.com'.split()) @mock.patch('certbot._internal.cert_manager.domains_for_certname') @mock.patch('certbot.display.ops.choose_names') @@ -200,14 +241,15 @@ def test_find_lineage_for_domains_new_certname(self, mock_report_cert, # no lineage with this name but we specified domains so create a new cert self._call(('certonly --webroot -d example.com -d test.com ' '--cert-name example.com').split()) - self.assertTrue(mock_lineage.call_count == 1) - self.assertTrue(mock_report_cert.call_count == 1) + self.assertEqual(mock_lineage.call_count, 1) + self.assertEqual(mock_report_cert.call_count, 1) # no lineage with this name and we didn't give domains mock_choose_names.return_value = ["somename"] mock_domains_for_certname.return_value = None self._call(('certonly --webroot --cert-name example.com').split()) - self.assertTrue(mock_choose_names.called) + self.assertIs(mock_choose_names.called, True) + class FindDomainsOrCertnameTest(unittest.TestCase): """Tests for certbot._internal.main._find_domains_or_certname.""" @@ -718,7 +760,7 @@ def test_configurator_selection(self, mock_exe_exists, _, __, ___): # This needed two calls to find_all(), which we're avoiding for now # because of possible side effects: # https://github.com/letsencrypt/letsencrypt/commit/51ed2b681f87b1eb29088dd48718a54f401e4855 - #with mock.patch('certbot._internal.cli.plugins_testable') as plugins: + # with mock.patch('certbot._internal.cli.plugins_testable') as plugins: # plugins.return_value = {"apache": True, "nginx": True} # ret, _, _, _ = self._call(args) # self.assertTrue("Too many flags setting" in ret) @@ -981,6 +1023,7 @@ def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None, mock_lineage.should_autorenew.return_value = due_for_renewal mock_lineage.has_pending_deployment.return_value = False mock_lineage.names.return_value = ['isnot.org'] + mock_lineage.private_key_type = 'RSA' mock_certr = mock.MagicMock() mock_key = mock.MagicMock(pem='pem_key') mock_client = mock.MagicMock() diff --git a/certbot/tests/plugins/common_test.py b/certbot/tests/plugins/common_test.py index 344e6312f91..bcd190e9b15 100644 --- a/certbot/tests/plugins/common_test.py +++ b/certbot/tests/plugins/common_test.py @@ -233,11 +233,11 @@ def test_get_addr_obj(self): def test_eq(self): self.assertEqual(self.addr1, self.addr2.get_addr_obj("")) self.assertNotEqual(self.addr1, self.addr2) - self.assertFalse(self.addr1 == 3333) + self.assertNotEqual(self.addr1, 3333) self.assertEqual(self.addr4, self.addr4.get_addr_obj("")) self.assertNotEqual(self.addr4, self.addr5) - self.assertFalse(self.addr4 == 3333) + self.assertNotEqual(self.addr4, 3333) from certbot.plugins.common import Addr self.assertEqual(self.addr4, Addr.fromstring("[fe00:0:0::1]")) self.assertEqual(self.addr4, Addr.fromstring("[fe00:0::0:0:1]")) diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py index 0957b5c31b4..44c78c701c1 100644 --- a/certbot/tests/renewal_test.py +++ b/certbot/tests/renewal_test.py @@ -74,6 +74,30 @@ def test_reuse_key_renewal_params(self): assert self.config.rsa_key_size == 2048 + def test_reuse_ec_key_renewal_params(self): + self.config.elliptic_curve = 'INVALID_CURVE' + self.config.reuse_key = True + self.config.dry_run = True + self.config.key_type = 'ecdsa' + config = configuration.NamespaceConfig(self.config) + + rc_path = test_util.make_lineage( + self.config.config_dir, + 'sample-renewal-ec.conf', + ec=True, + ) + lineage = storage.RenewableCert(rc_path, config) + + le_client = mock.MagicMock() + le_client.obtain_certificate.return_value = (None, None, None, None) + + from certbot._internal import renewal + + with mock.patch('certbot._internal.renewal.hooks.renew_hook'): + renewal.renew_cert(self.config, None, le_client, lineage) + + assert self.config.elliptic_curve == 'secp256r1' + class RestoreRequiredConfigElementsTest(test_util.ConfigTestCase): """Tests for certbot._internal.renewal.restore_required_config_elements.""" @@ -139,5 +163,70 @@ def test_ancient_server_renewal_conf(self, mock_set_by_cli): self.assertEqual(self.config.server, constants.CLI_DEFAULTS['server']) +class DescribeResultsTest(unittest.TestCase): + """Tests for certbot._internal.renewal._renew_describe_results.""" + def setUp(self): + self.patchers = { + 'log_error': mock.patch('certbot._internal.renewal.logger.error'), + 'notify': mock.patch('certbot._internal.renewal.display_util.notify')} + self.mock_notify = self.patchers['notify'].start() + self.mock_error = self.patchers['log_error'].start() + + def tearDown(self): + for patch in self.patchers.values(): + patch.stop() + + @classmethod + def _call(cls, *args, **kwargs): + from certbot._internal.renewal import _renew_describe_results + _renew_describe_results(*args, **kwargs) + + def _assert_success_output(self, lines): + self.mock_notify.assert_has_calls([mock.call(l) for l in lines]) + + def test_no_renewal_attempts(self): + self._call(mock.MagicMock(dry_run=True), [], [], [], []) + self._assert_success_output(['No simulated renewals were attempted.']) + + def test_successful_renewal(self): + self._call(mock.MagicMock(dry_run=False), ['good.pem'], None, None, None) + self._assert_success_output([ + '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', + 'Congratulations, all renewals succeeded: ', + ' good.pem (success)', + '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', + ]) + + def test_failed_renewal(self): + self._call(mock.MagicMock(dry_run=False), [], ['bad.pem'], [], []) + self._assert_success_output([ + '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', + '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', + ]) + self.mock_error.assert_has_calls([ + mock.call('All %ss failed. The following certs could not be renewed:', 'renewal'), + mock.call(' bad.pem (failure)'), + ]) + + def test_all_renewal(self): + self._call(mock.MagicMock(dry_run=True), + ['good.pem', 'good2.pem'], ['bad.pem', 'bad2.pem'], + ['foo.pem expires on 123'], ['errored.conf']) + self._assert_success_output([ + '\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', + 'The following certs are not due for renewal yet:', + ' foo.pem expires on 123 (skipped)', + 'The following simulated renewals succeeded:', + ' good.pem (success)\n good2.pem (success)\n', + '\nAdditionally, the following renewal configurations were invalid: ', + ' errored.conf (parsefail)', + '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', + ]) + self.mock_error.assert_has_calls([ + mock.call('The following %ss failed:', 'simulated renewal'), + mock.call(' bad.pem (failure)\n bad2.pem (failure)'), + ]) + + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index d28d7df6f5a..4b93d2c3838 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -127,7 +127,7 @@ def test_it(self, mock_register, mock_logger): from certbot import util # Despite lock_dir_until_exit has been called twice to subdir, its lock should have been # added only once. So we expect to have two lock references: for self.tempdir and subdir - self.assertTrue(len(util._LOCKS) == 2) # pylint: disable=protected-access + self.assertEqual(len(util._LOCKS), 2) # pylint: disable=protected-access registered_func() # Exception should not be raised # Logically, logger.debug, that would be invoked in case of unlock failure, # should never been called. diff --git a/letsencrypt-auto b/letsencrypt-auto index 2a0cda9b332..ee12d470690 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="1.9.0" +LE_AUTO_VERSION="1.10.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -799,11 +799,7 @@ BootstrapMageiaCommon() { # that function. If Bootstrap is set to a function that doesn't install any # packages BOOTSTRAP_VERSION is not set. if [ -f /etc/debian_version ]; then - Bootstrap() { - BootstrapMessage "Debian-based OSes" - BootstrapDebCommon - } - BOOTSTRAP_VERSION="BootstrapDebCommon $BOOTSTRAP_DEB_COMMON_VERSION" + DEPRECATED_OS=1 elif [ -f /etc/mageia-release ]; then # Mageia has both /etc/mageia-release and /etc/redhat-release DEPRECATED_OS=1 @@ -1497,18 +1493,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==1.9.0 \ - --hash=sha256:d5a804d32e471050921f7b39ed9859e2e9de02824176ed78f57266222036b53a \ - --hash=sha256:2ff9bf7d9af381c7efee22dec2dd6938d9d8fddcc9e11682b86e734164a30b57 -acme==1.9.0 \ - --hash=sha256:d8061b396a22b21782c9b23ff9a945b23e50fca2573909a42f845e11d5658ac5 \ - --hash=sha256:38a1630c98e144136c62eec4d2c545a1bdb1a3cd4eca82214be6b83a1f5a161f -certbot-apache==1.9.0 \ - --hash=sha256:09528a820d57e54984d490100644cd8a6603db97bf5776f86e95795ecfacf23d \ - --hash=sha256:f47fb3f4a9bd927f4812121a0beefe56b163475a28f4db34c64dc838688d9e9e -certbot-nginx==1.9.0 \ - --hash=sha256:bb2e3f7fe17f071f350a3efa48571b8ef40a8e4b6db9c6da72539206a20b70be \ - --hash=sha256:ab26a4f49d53b0e8bf0f903e58e2a840cda233fe1cbbc54c36ff17f973e57d65 +certbot==1.10.1 \ + --hash=sha256:011ac980fa21b9f29e02c9b8d8b86e8a4bf4670b51b6ad91656e401e9d2d2231 \ + --hash=sha256:0d9ee3fc09e0d03b2d1b1f1c4916e61ecfc6904b4216ddef4e6a5ca1424d9cb7 +acme==1.10.1 \ + --hash=sha256:752d598e54e98ad1e874de53fd50c61044f1b566d6deb790db5676ce9c573546 \ + --hash=sha256:fcbb559aedc96b404edf593e78517dcd7291984d5a37036c3fc77f3c5c122fd8 +certbot-apache==1.10.1 \ + --hash=sha256:f077b4b7f166627ef5e0921fe7cde57700670fc86e9ad9dbdfaf2c573cc0f2fa \ + --hash=sha256:97ed637b4c7b03820db6c69aa90145dc989933351d46a3d62baf6b71674f0a10 +certbot-nginx==1.10.1 \ + --hash=sha256:7c36459021f8a1ec3b6c062e4c4fc866bfaa1dbf26ccd29e043dd6848003be08 \ + --hash=sha256:c0bbeccf85f46b728fd95e6bb8c2649d32d3383d7f47ea4b9c312d12bf04d2f0 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/Dockerfile.redhat6 b/letsencrypt-auto-source/Dockerfile.redhat6 deleted file mode 100644 index 66f21bc140b..00000000000 --- a/letsencrypt-auto-source/Dockerfile.redhat6 +++ /dev/null @@ -1,54 +0,0 @@ -# For running tests, build a docker image with a passwordless sudo and a trust -# store we can manipulate. - -ARG REDHAT_DIST_FLAVOR -FROM ${REDHAT_DIST_FLAVOR}:6 - -ARG REDHAT_DIST_FLAVOR - -RUN curl -O https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm \ - && rpm -ivh epel-release-latest-6.noarch.rpm - -# Install pip and sudo: -RUN yum install -y python-pip sudo -# Update to a stable and tested version of pip. -# We do not use pipstrap here because it no longer supports Python 2.6. -RUN pip install pip==9.0.1 setuptools==29.0.1 wheel==0.29.0 -# Pin pytest version for increased stability -RUN pip install pytest==3.2.5 six==1.10.0 - -# Add an unprivileged user: -RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups wheel --uid 1000 lea - -# Let that user sudo: -RUN sed -i.bkp -e \ - 's/# %wheel\(NOPASSWD: ALL\)\?/%wheel/g' \ - /etc/sudoers - -RUN mkdir -p /home/lea/certbot - -# Install fake testing CA: -COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/ -RUN update-ca-trust - -# Copy current letsencrypt-auto: -COPY . /home/lea/certbot/letsencrypt-auto-source - -# Tweak uname binary for tests on fake 32bits -COPY tests/uname_wrapper.sh /bin -RUN mv /bin/uname /bin/uname_orig \ - && mv /bin/uname_wrapper.sh /bin/uname \ - && chmod +x /bin/uname - -# Fetch previous letsencrypt-auto that was installing python 3.4 -RUN curl https://raw.githubusercontent.com/certbot/certbot/v0.38.0/letsencrypt-auto-source/letsencrypt-auto \ - -o /home/lea/certbot/letsencrypt-auto-source/letsencrypt-auto_py_34 \ - && chmod +x /home/lea/certbot/letsencrypt-auto-source/letsencrypt-auto_py_34 - -RUN cp /home/lea/certbot/letsencrypt-auto-source/tests/${REDHAT_DIST_FLAVOR}6_tests.sh /home/lea/certbot/letsencrypt-auto-source/tests/redhat6_tests.sh \ - && chmod +x /home/lea/certbot/letsencrypt-auto-source/tests/redhat6_tests.sh - -USER lea -WORKDIR /home/lea - -CMD ["sudo", "certbot/letsencrypt-auto-source/tests/redhat6_tests.sh"] diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index f73e8b35e37..c1897074c35 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- -iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAl98wk8ACgkQTRfJlc2X -dfIctgf/TO83xXJJ8haqxke0ehHCwcmipX7ijPhwvaUTSqciMa56KnGJLNp1lAVz -vv8sfHUf7NSvGlRg+5M0szWY25+JzveJDNzse3rOzFmxA1GNKUycE3/zE/IdBRwN -fmxJHaUBrBL2erBZPHe8gFGTvlzopBoGSmQpWGY3hIufPWKBJohCbTscKbaa9hyz -njmMvwRdeqzvLWVZ4jNDDsil9kKl2Emue3guzA/cvVxHe17DZyLDfqni7ysZIcTn -wPAQzpLBKHyiqVRoVk+BJ6Z6wamW4NAxKbjXy9GrHy4txlfW8tGd3jXha8yWqJeH -xEFK02Zp+T17+C5uqEW4o0cIofMjCw== -=9UGf +iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAl/JL3kACgkQTRfJlc2X +dfKJMwf/RXjfg5KScEjWiR+YMAcTVxGl4ITDMNBvmPoqCfrPwIJQewy1k6yQUITr +tMe0tkPneGgGccJreLAuO4+RdmNqm2MKBO3wMW9YZobJxcbMmrtVxyBD2OP4K/lL +oCZvjcN5pLvje6OlMwJ/fQ+zGY8mFUpfKIluxKrqkkO3p6Q+i/wPXF5Gjjb2J/bI +N+TczQJYUkDWAw7Tp4ho3J9xpqIn3zyOc2hI3wQDMC1o9sU5a80Vyc/mEqpE8SQ3 +qOWg9Gdx3DXTWOztcx2IxZtFEkIukPM8iD/Fkr//3XHeIc3+mqRAQdY+w7EopzbP +hLwjHVEJs1EMYq8ntWmMFjZ4+ImFgw== +=Peuv -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 83a1041baec..7f358f80592 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="1.10.0.dev0" +LE_AUTO_VERSION="1.11.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -804,6 +804,7 @@ elif [ -f /etc/mageia-release ]; then # Mageia has both /etc/mageia-release and /etc/redhat-release DEPRECATED_OS=1 elif [ -f /etc/redhat-release ]; then + DEPRECATED_OS=1 # Run DeterminePythonVersion to decide on the basis of available Python versions # whether to use 2.x or 3.x on RedHat-like systems. # Then, revert LE_PYTHON to its previous state. @@ -836,12 +837,7 @@ elif [ -f /etc/redhat-release ]; then INTERACTIVE_BOOTSTRAP=1 fi - Bootstrap() { - BootstrapMessage "Legacy RedHat-based OSes that will use Python3" - BootstrapRpmPython3Legacy - } USE_PYTHON_3=1 - BOOTSTRAP_VERSION="BootstrapRpmPython3Legacy $BOOTSTRAP_RPM_PYTHON3_LEGACY_VERSION" # Try now to enable SCL rh-python36 for systems already bootstrapped # NB: EnablePython36SCL has been defined along with BootstrapRpmPython3Legacy in certbot-auto @@ -860,18 +856,7 @@ elif [ -f /etc/redhat-release ]; then fi if [ "$RPM_USE_PYTHON_3" = 1 ]; then - Bootstrap() { - BootstrapMessage "RedHat-based OSes that will use Python3" - BootstrapRpmPython3 - } USE_PYTHON_3=1 - BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION" - else - Bootstrap() { - BootstrapMessage "RedHat-based OSes" - BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" fi fi @@ -889,10 +874,7 @@ elif uname | grep -iq FreeBSD ; then elif uname | grep -iq Darwin ; then DEPRECATED_OS=1 elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then - Bootstrap() { - ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + DEPRECATED_OS=1 elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then DEPRECATED_OS=1 else @@ -1493,18 +1475,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==1.9.0 \ - --hash=sha256:d5a804d32e471050921f7b39ed9859e2e9de02824176ed78f57266222036b53a \ - --hash=sha256:2ff9bf7d9af381c7efee22dec2dd6938d9d8fddcc9e11682b86e734164a30b57 -acme==1.9.0 \ - --hash=sha256:d8061b396a22b21782c9b23ff9a945b23e50fca2573909a42f845e11d5658ac5 \ - --hash=sha256:38a1630c98e144136c62eec4d2c545a1bdb1a3cd4eca82214be6b83a1f5a161f -certbot-apache==1.9.0 \ - --hash=sha256:09528a820d57e54984d490100644cd8a6603db97bf5776f86e95795ecfacf23d \ - --hash=sha256:f47fb3f4a9bd927f4812121a0beefe56b163475a28f4db34c64dc838688d9e9e -certbot-nginx==1.9.0 \ - --hash=sha256:bb2e3f7fe17f071f350a3efa48571b8ef40a8e4b6db9c6da72539206a20b70be \ - --hash=sha256:ab26a4f49d53b0e8bf0f903e58e2a840cda233fe1cbbc54c36ff17f973e57d65 +certbot==1.10.1 \ + --hash=sha256:011ac980fa21b9f29e02c9b8d8b86e8a4bf4670b51b6ad91656e401e9d2d2231 \ + --hash=sha256:0d9ee3fc09e0d03b2d1b1f1c4916e61ecfc6904b4216ddef4e6a5ca1424d9cb7 +acme==1.10.1 \ + --hash=sha256:752d598e54e98ad1e874de53fd50c61044f1b566d6deb790db5676ce9c573546 \ + --hash=sha256:fcbb559aedc96b404edf593e78517dcd7291984d5a37036c3fc77f3c5c122fd8 +certbot-apache==1.10.1 \ + --hash=sha256:f077b4b7f166627ef5e0921fe7cde57700670fc86e9ad9dbdfaf2c573cc0f2fa \ + --hash=sha256:97ed637b4c7b03820db6c69aa90145dc989933351d46a3d62baf6b71674f0a10 +certbot-nginx==1.10.1 \ + --hash=sha256:7c36459021f8a1ec3b6c062e4c4fc866bfaa1dbf26ccd29e043dd6848003be08 \ + --hash=sha256:c0bbeccf85f46b728fd95e6bb8c2649d32d3383d7f47ea4b9c312d12bf04d2f0 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 66b2b2084ee..c701f4a4d72 100644 Binary files a/letsencrypt-auto-source/letsencrypt-auto.sig and b/letsencrypt-auto-source/letsencrypt-auto.sig differ diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 5eb82b70599..bc27469fbb8 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -326,6 +326,7 @@ elif [ -f /etc/mageia-release ]; then # Mageia has both /etc/mageia-release and /etc/redhat-release DEPRECATED_OS=1 elif [ -f /etc/redhat-release ]; then + DEPRECATED_OS=1 # Run DeterminePythonVersion to decide on the basis of available Python versions # whether to use 2.x or 3.x on RedHat-like systems. # Then, revert LE_PYTHON to its previous state. @@ -358,12 +359,7 @@ elif [ -f /etc/redhat-release ]; then INTERACTIVE_BOOTSTRAP=1 fi - Bootstrap() { - BootstrapMessage "Legacy RedHat-based OSes that will use Python3" - BootstrapRpmPython3Legacy - } USE_PYTHON_3=1 - BOOTSTRAP_VERSION="BootstrapRpmPython3Legacy $BOOTSTRAP_RPM_PYTHON3_LEGACY_VERSION" # Try now to enable SCL rh-python36 for systems already bootstrapped # NB: EnablePython36SCL has been defined along with BootstrapRpmPython3Legacy in certbot-auto @@ -382,18 +378,7 @@ elif [ -f /etc/redhat-release ]; then fi if [ "$RPM_USE_PYTHON_3" = 1 ]; then - Bootstrap() { - BootstrapMessage "RedHat-based OSes that will use Python3" - BootstrapRpmPython3 - } USE_PYTHON_3=1 - BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION" - else - Bootstrap() { - BootstrapMessage "RedHat-based OSes" - BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" fi fi @@ -411,10 +396,7 @@ elif uname | grep -iq FreeBSD ; then elif uname | grep -iq Darwin ; then DEPRECATED_OS=1 elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then - Bootstrap() { - ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + DEPRECATED_OS=1 elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then DEPRECATED_OS=1 else diff --git a/letsencrypt-auto-source/pieces/certbot-requirements.txt b/letsencrypt-auto-source/pieces/certbot-requirements.txt index 2fd82eb26c0..4839d1e724c 100644 --- a/letsencrypt-auto-source/pieces/certbot-requirements.txt +++ b/letsencrypt-auto-source/pieces/certbot-requirements.txt @@ -1,12 +1,12 @@ -certbot==1.9.0 \ - --hash=sha256:d5a804d32e471050921f7b39ed9859e2e9de02824176ed78f57266222036b53a \ - --hash=sha256:2ff9bf7d9af381c7efee22dec2dd6938d9d8fddcc9e11682b86e734164a30b57 -acme==1.9.0 \ - --hash=sha256:d8061b396a22b21782c9b23ff9a945b23e50fca2573909a42f845e11d5658ac5 \ - --hash=sha256:38a1630c98e144136c62eec4d2c545a1bdb1a3cd4eca82214be6b83a1f5a161f -certbot-apache==1.9.0 \ - --hash=sha256:09528a820d57e54984d490100644cd8a6603db97bf5776f86e95795ecfacf23d \ - --hash=sha256:f47fb3f4a9bd927f4812121a0beefe56b163475a28f4db34c64dc838688d9e9e -certbot-nginx==1.9.0 \ - --hash=sha256:bb2e3f7fe17f071f350a3efa48571b8ef40a8e4b6db9c6da72539206a20b70be \ - --hash=sha256:ab26a4f49d53b0e8bf0f903e58e2a840cda233fe1cbbc54c36ff17f973e57d65 +certbot==1.10.1 \ + --hash=sha256:011ac980fa21b9f29e02c9b8d8b86e8a4bf4670b51b6ad91656e401e9d2d2231 \ + --hash=sha256:0d9ee3fc09e0d03b2d1b1f1c4916e61ecfc6904b4216ddef4e6a5ca1424d9cb7 +acme==1.10.1 \ + --hash=sha256:752d598e54e98ad1e874de53fd50c61044f1b566d6deb790db5676ce9c573546 \ + --hash=sha256:fcbb559aedc96b404edf593e78517dcd7291984d5a37036c3fc77f3c5c122fd8 +certbot-apache==1.10.1 \ + --hash=sha256:f077b4b7f166627ef5e0921fe7cde57700670fc86e9ad9dbdfaf2c573cc0f2fa \ + --hash=sha256:97ed637b4c7b03820db6c69aa90145dc989933351d46a3d62baf6b71674f0a10 +certbot-nginx==1.10.1 \ + --hash=sha256:7c36459021f8a1ec3b6c062e4c4fc866bfaa1dbf26ccd29e043dd6848003be08 \ + --hash=sha256:c0bbeccf85f46b728fd95e6bb8c2649d32d3383d7f47ea4b9c312d12bf04d2f0 diff --git a/letsencrypt-auto-source/tests/__init__.py b/letsencrypt-auto-source/tests/__init__.py deleted file mode 100644 index 8a1613aa548..00000000000 --- a/letsencrypt-auto-source/tests/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Tests for letsencrypt-auto - -Run these locally by saying... :: - - ./build.py && docker build -t lea . -f Dockerfile. && docker run --rm -t -i lea - -""" diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py deleted file mode 100644 index 805bb21af6b..00000000000 --- a/letsencrypt-auto-source/tests/auto_test.py +++ /dev/null @@ -1,503 +0,0 @@ -"""Tests for letsencrypt-auto""" - -from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler -from contextlib import contextmanager -from functools import partial -from json import dumps -from os import chmod, environ, makedirs, stat -from os.path import abspath, dirname, exists, join -import re -from shutil import copy, rmtree -import socket -import ssl -from stat import S_IMODE, S_IRUSR, S_IWUSR, S_IXUSR, S_IWGRP, S_IWOTH -from subprocess import CalledProcessError, Popen, PIPE -import sys -from tempfile import mkdtemp -from threading import Thread -from unittest import TestCase - -from pytest import mark -from six.moves import xrange # pylint: disable=redefined-builtin - - -@mark.skip -def tests_dir(): - """Return a path to the "tests" directory.""" - return dirname(abspath(__file__)) - - -def copy_stable(src, dst): - """ - Copy letsencrypt-auto, and replace its current version to its equivalent stable one. - This is needed to test correctly the self-upgrade functionality. - """ - copy(src, dst) - with open(dst, 'r') as file: - filedata = file.read() - filedata = re.sub(r'LE_AUTO_VERSION="(.*)\.dev0"', r'LE_AUTO_VERSION="\1"', filedata) - with open(dst, 'w') as file: - file.write(filedata) - - -sys.path.insert(0, dirname(tests_dir())) -from build import build as build_le_auto - - -BOOTSTRAP_FILENAME = 'certbot-auto-bootstrap-version.txt' -"""Name of the file where certbot-auto saves its bootstrap version.""" - - -class RequestHandler(BaseHTTPRequestHandler): - """An HTTPS request handler which is quiet and serves a specific folder.""" - - def __init__(self, resources, *args, **kwargs): - """ - :arg resources: A dict of resource paths pointing to content bytes - - """ - self.resources = resources - BaseHTTPRequestHandler.__init__(self, *args, **kwargs) - - def log_message(self, format, *args): - """Don't log each request to the terminal.""" - - def do_GET(self): - """Serve a GET request.""" - content = self.send_head() - if content is not None: - self.wfile.write(content) - - def send_head(self): - """Common code for GET and HEAD commands - - This sends the response code and MIME headers and returns either a - bytestring of content or, if none is found, None. - - """ - path = self.path[1:] # Strip leading slash. - content = self.resources.get(path) - if content is None: - self.send_error(404, 'Path "%s" not found in self.resources' % path) - else: - self.send_response(200) - self.send_header('Content-type', 'text/plain') - self.send_header('Content-Length', str(len(content))) - self.end_headers() - return content - - -def server_and_port(resources): - """Return an unstarted HTTPS server and the port it will use.""" - # Find a port, and bind to it. I can't get the OS to close the socket - # promptly after we shut down the server, so we typically need to try - # a couple ports after the first test case. Setting - # TCPServer.allow_reuse_address = True seems to have nothing to do - # with this behavior. - worked = False - for port in xrange(4443, 4543): - try: - server = HTTPServer(('localhost', port), - partial(RequestHandler, resources)) - except socket.error: - pass - else: - worked = True - server.socket = ssl.wrap_socket( - server.socket, - certfile=join(tests_dir(), 'certs', 'localhost', 'server.pem'), - server_side=True) - break - if not worked: - raise RuntimeError("Couldn't find an unused socket for the testing HTTPS server.") - return server, port - - -@contextmanager -def serving(resources): - """Spin up a local HTTPS server, and yield its base URL. - - Use a self-signed cert generated as outlined by - https://coolaj86.com/articles/create-your-own-certificate-authority-for- - testing/. - - """ - server, port = server_and_port(resources) - thread = Thread(target=server.serve_forever) - try: - thread.start() - yield 'https://localhost:{port}/'.format(port=port) - finally: - server.shutdown() - thread.join() - - -LE_AUTO_PATH = join(dirname(tests_dir()), 'letsencrypt-auto') - - -@contextmanager -def temp_paths(): - """Creates and deletes paths for letsencrypt-auto and its venv.""" - dir = mkdtemp(prefix='le-test-') - try: - yield join(dir, 'letsencrypt-auto'), join(dir, 'venv') - finally: - rmtree(dir, ignore_errors=True) - - -def out_and_err(command, input=None, shell=False, env=None): - """Run a shell command, and return stderr and stdout as string. - - If the command returns nonzero, raise CalledProcessError. - - :arg command: A list of commandline args - :arg input: Data to pipe to stdin. Omit for none. - - Remaining args have the same meaning as for Popen. - - """ - process = Popen(command, - stdout=PIPE, - stdin=PIPE, - stderr=PIPE, - shell=shell, - env=env) - out, err = process.communicate(input=input) - status = process.poll() # same as in check_output(), though wait() sounds better - if status: - error = CalledProcessError(status, command) - error.output = out - print('stdout output was:') - print(out) - print('stderr output was:') - print(err) - raise error - return out, err - - -def signed(content, private_key_name='signing.key'): - """Return the signed SHA-256 hash of ``content``, using the given key file.""" - command = ['openssl', 'dgst', '-sha256', '-sign', - join(tests_dir(), private_key_name)] - out, err = out_and_err(command, input=content) - return out - - -def install_le_auto(contents, install_path): - """Install some given source code as the letsencrypt-auto script at the - root level of a virtualenv. - - :arg contents: The contents of the built letsencrypt-auto script - :arg install_path: The path where to install the script - - """ - with open(install_path, 'w') as le_auto: - le_auto.write(contents) - chmod(install_path, S_IRUSR | S_IXUSR) - - -def run_le_auto(le_auto_path, venv_dir, base_url=None, le_auto_args_str='--version', **kwargs): - """Run the prebuilt version of letsencrypt-auto, returning stdout and - stderr strings. - - If the command returns other than 0, raise CalledProcessError. - - """ - env = environ.copy() - d = dict(VENV_PATH=venv_dir, - NO_CERT_VERIFY='1', - **kwargs) - - if base_url is not None: - # URL to PyPI-style JSON that tell us the latest released version - # of LE: - d['LE_AUTO_JSON_URL'] = base_url + 'certbot/json' - # URL to dir containing letsencrypt-auto and letsencrypt-auto.sig: - d['LE_AUTO_DIR_TEMPLATE'] = base_url + '%s/' - # The public key corresponding to signing.key: - d['LE_AUTO_PUBLIC_KEY'] = """-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMoSzLYQ7E1sdSOkwelg -tzKIh2qi3bpXuYtcfFC0XrvWig071NwIj+dZiT0OLZ2hPispEH0B7ISuuWg1ll7G -hFW0VdbxL6JdGzS2ShNWkX9hE9z+j8VqwDPOBn3ZHm03qwpYkBDwQib3KqOdYbTT -uUtJmmGcuk3a9Aq/sCT6DdfmTSdP5asdQYwIcaQreDrOosaS84DTWI3IU+UYJVgl -LsIVPBuy9IcgHidUQ96hJnoPsDCWsHwX62495QKEarauyKQrJzFes0EY95orDM47 -Z5o/NDiQB11m91yNB0MmPYY9QSbnOA9j7IaaC97AwRLuwXY+/R2ablTcxurWou68 -iQIDAQAB ------END PUBLIC KEY-----""" - - env.update(d) - - return out_and_err( - le_auto_path + ' ' + le_auto_args_str, - shell=True, - env=env) - - -def set_le_script_version(venv_dir, version): - """Tell the letsencrypt script to report a certain version. - - We actually replace the script with a dummy version that knows only how to - print its version. - - """ - letsencrypt_path = join(venv_dir, 'bin', 'letsencrypt') - with open(letsencrypt_path, 'w') as script: - script.write("#!/usr/bin/env python\n" - "from sys import stderr\n" - "stderr.write('letsencrypt %s\\n')" % version) - chmod(letsencrypt_path, S_IRUSR | S_IXUSR) - - -def sudo_chmod(path, mode): - """Runs `sudo chmod mode path`.""" - mode = oct(mode).replace('o', '') - out_and_err(['sudo', 'chmod', mode, path]) - - -class AutoTests(TestCase): - """Test the major branch points of letsencrypt-auto: - - * An le-auto upgrade is needed. - * An le-auto upgrade is not needed. - * There was an out-of-date LE script installed. - * There was a current LE script installed. - * There was no LE script installed (less important). - * Pip hash-verification passes. - * Pip has a hash mismatch. - * The OpenSSL sig matches. - * The OpenSSL sig mismatches. - - For tests which get to the end, we run merely ``letsencrypt --version``. - The functioning of the rest of the certbot script is covered by other - test suites. - - """ - NEW_LE_AUTO = build_le_auto( - version='99.9.9', - requirements='letsencrypt==99.9.9 --hash=sha256:1cc14d61ab424cdee446f51e50f1123f8482ec740587fe78626c933bba2873a0') - NEW_LE_AUTO_SIG = signed(NEW_LE_AUTO) - - def test_successes(self): - """Exercise most branches of letsencrypt-auto. - - They just happen to be the branches in which everything goes well. - - I violate my usual rule of having small, decoupled tests, because... - - 1. We shouldn't need to run a Cartesian product of the branches: the - phases run in separate shell processes, containing state leakage - pretty effectively. The only shared state is FS state, and it's - limited to a temp dir, assuming (if we dare) all functions properly. - 2. One combination of branches happens to set us up nicely for testing - the next, saving code. - - """ - with temp_paths() as (le_auto_path, venv_dir): - # This serves a PyPI page with a higher version, a GitHub-alike - # with a corresponding le-auto script, and a matching signature. - resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO, - 'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG} - with serving(resources) as base_url: - run_letsencrypt_auto = partial( - run_le_auto, - le_auto_path, - venv_dir, - base_url, - PIP_FIND_LINKS=join(tests_dir(), - 'fake-letsencrypt', - 'dist')) - - # Test when a phase-1 upgrade is needed, there's no LE binary - # installed, and pip hashes verify: - install_le_auto(build_le_auto(version='50.0.0'), le_auto_path) - out, err = run_letsencrypt_auto() - self.assertTrue(re.match(r'letsencrypt \d+\.\d+\.\d+', - err.strip().splitlines()[-1])) - # Make a few assertions to test the validity of the next tests: - self.assertTrue('Upgrading certbot-auto ' in out) - self.assertTrue('Creating virtual environment...' in out) - - # Now we have le-auto 99.9.9 and LE 99.9.9 installed. This - # conveniently sets us up to test the next 2 cases. - - # Test when neither phase-1 upgrade nor phase-2 upgrade is - # needed (probably a common case): - out, err = run_letsencrypt_auto() - self.assertFalse('Upgrading certbot-auto ' in out) - self.assertFalse('Creating virtual environment...' in out) - - def test_phase2_upgrade(self): - """Test a phase-2 upgrade without a phase-1 upgrade.""" - resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO, - 'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG} - with serving(resources) as base_url: - pip_find_links=join(tests_dir(), 'fake-letsencrypt', 'dist') - with temp_paths() as (le_auto_path, venv_dir): - install_le_auto(self.NEW_LE_AUTO, le_auto_path) - - # Create venv saving the correct bootstrap script version - out, err = run_le_auto(le_auto_path, venv_dir, base_url, - PIP_FIND_LINKS=pip_find_links) - self.assertFalse('Upgrading certbot-auto ' in out) - self.assertTrue('Creating virtual environment...' in out) - with open(join(venv_dir, BOOTSTRAP_FILENAME)) as f: - bootstrap_version = f.read() - - # Create a new venv with an old letsencrypt version - with temp_paths() as (le_auto_path, venv_dir): - venv_bin = join(venv_dir, 'bin') - makedirs(venv_bin) - set_le_script_version(venv_dir, '0.0.1') - with open(join(venv_dir, BOOTSTRAP_FILENAME), 'w') as f: - f.write(bootstrap_version) - - install_le_auto(self.NEW_LE_AUTO, le_auto_path) - out, err = run_le_auto(le_auto_path, venv_dir, base_url, - PIP_FIND_LINKS=pip_find_links) - - self.assertFalse('Upgrading certbot-auto ' in out) - self.assertTrue('Creating virtual environment...' in out) - - def test_openssl_failure(self): - """Make sure we stop if the openssl signature check fails.""" - with temp_paths() as (le_auto_path, venv_dir): - # Serve an unrelated hash signed with the good key (easier than - # making a bad key, and a mismatch is a mismatch): - resources = {'': 'certbot/', - 'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), - 'v99.9.9/letsencrypt-auto.sig': signed('something else')} - with serving(resources) as base_url: - copy_stable(LE_AUTO_PATH, le_auto_path) - try: - out, err = run_le_auto(le_auto_path, venv_dir, base_url) - except CalledProcessError as exc: - self.assertEqual(exc.returncode, 1) - self.assertTrue("Couldn't verify signature of downloaded " - "certbot-auto." in exc.output) - else: - print(out) - self.fail('Signature check on certbot-auto erroneously passed.') - - def test_pip_failure(self): - """Make sure pip stops us if there is a hash mismatch.""" - with temp_paths() as (le_auto_path, venv_dir): - resources = {'': 'certbot/', - 'certbot/json': dumps({'releases': {'99.9.9': None}})} - with serving(resources) as base_url: - # Build a le-auto script embedding a bad requirements file: - install_le_auto( - build_le_auto( - version='99.9.9', - requirements='configobj==5.0.6 --hash=sha256:badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb'), - le_auto_path) - try: - out, err = run_le_auto(le_auto_path, venv_dir, base_url) - except CalledProcessError as exc: - self.assertEqual(exc.returncode, 1) - self.assertTrue("THESE PACKAGES DO NOT MATCH THE HASHES " - "FROM THE REQUIREMENTS FILE" in exc.output) - self.assertFalse( - exists(venv_dir), - msg="The virtualenv was left around, even though " - "installation didn't succeed. We shouldn't do " - "this, as it foils our detection of whether we " - "need to recreate the virtualenv, which hinges " - "on the presence of $VENV_BIN/letsencrypt.") - else: - self.fail("Pip didn't detect a bad hash and stop the " - "installation.") - - def test_permissions_warnings(self): - """Make sure letsencrypt-auto properly warns about permissions problems.""" - # This test assumes that only the parent of the directory containing - # letsencrypt-auto (usually /tmp) may have permissions letsencrypt-auto - # considers insecure. - with temp_paths() as (le_auto_path, venv_dir): - le_auto_path = abspath(le_auto_path) - le_auto_dir = dirname(le_auto_path) - le_auto_dir_parent = dirname(le_auto_dir) - install_le_auto(self.NEW_LE_AUTO, le_auto_path) - - run_letsencrypt_auto = partial( - run_le_auto, le_auto_path, venv_dir, - le_auto_args_str='--install-only --no-self-upgrade', - PIP_FIND_LINKS=join(tests_dir(), 'fake-letsencrypt', 'dist')) - # Run letsencrypt-auto once with current permissions to avoid - # potential problems when the script tries to write to temporary - # directories. - run_letsencrypt_auto() - - le_auto_dir_mode = stat(le_auto_dir).st_mode - le_auto_dir_parent_mode = S_IMODE(stat(le_auto_dir_parent).st_mode) - try: - # Make letsencrypt-auto happy with the current permissions - chmod(le_auto_dir, S_IRUSR | S_IXUSR) - sudo_chmod(le_auto_dir_parent, 0o755) - - self._test_permissions_warnings_about_path(le_auto_path, run_letsencrypt_auto) - self._test_permissions_warnings_about_path(le_auto_dir, run_letsencrypt_auto) - finally: - chmod(le_auto_dir, le_auto_dir_mode) - sudo_chmod(le_auto_dir_parent, le_auto_dir_parent_mode) - - def _test_permissions_warnings_about_path(self, path, run_le_auto_func): - # Test that there are no problems with the current permissions - out, _ = run_le_auto_func() - self.assertFalse('insecure permissions' in out) - - stat_result = stat(path) - original_mode = stat_result.st_mode - - # Test world permissions - chmod(path, original_mode | S_IWOTH) - out, _ = run_le_auto_func() - self.assertTrue('insecure permissions' in out) - - # Test group permissions - if stat_result.st_gid >= 1000: - chmod(path, original_mode | S_IWGRP) - out, _ = run_le_auto_func() - self.assertTrue('insecure permissions' in out) - - # Test owner permissions - if stat_result.st_uid >= 1000: - chmod(path, original_mode | S_IWUSR) - out, _ = run_le_auto_func() - self.assertTrue('insecure permissions' in out) - - # Test that permissions were properly restored - chmod(path, original_mode) - out, _ = run_le_auto_func() - self.assertFalse('insecure permissions' in out) - - def test_disabled_permissions_warnings(self): - """Make sure that letsencrypt-auto permissions warnings can be disabled.""" - with temp_paths() as (le_auto_path, venv_dir): - le_auto_path = abspath(le_auto_path) - install_le_auto(self.NEW_LE_AUTO, le_auto_path) - - le_auto_args_str='--install-only --no-self-upgrade' - pip_links=join(tests_dir(), 'fake-letsencrypt', 'dist') - out, _ = run_le_auto(le_auto_path, venv_dir, - le_auto_args_str=le_auto_args_str, - PIP_FIND_LINKS=pip_links) - self.assertTrue('insecure permissions' in out) - - # Test that warnings are disabled when the script isn't run as - # root. - out, _ = run_le_auto(le_auto_path, venv_dir, - le_auto_args_str=le_auto_args_str, - LE_AUTO_SUDO='', - PIP_FIND_LINKS=pip_links) - self.assertFalse('insecure permissions' in out) - - # Test that --no-permissions-check disables warnings. - le_auto_args_str += ' --no-permissions-check' - out, _ = run_le_auto( - le_auto_path, venv_dir, - le_auto_args_str=le_auto_args_str, - PIP_FIND_LINKS=pip_links) - self.assertFalse('insecure permissions' in out) diff --git a/letsencrypt-auto-source/tests/centos6_tests.sh b/letsencrypt-auto-source/tests/centos6_tests.sh deleted file mode 100644 index 8bdffec87f7..00000000000 --- a/letsencrypt-auto-source/tests/centos6_tests.sh +++ /dev/null @@ -1,173 +0,0 @@ -#!/bin/bash -set -e -# Start by making sure your system is up-to-date: -yum update -y >/dev/null -yum install -y centos-release-scl >/dev/null -yum install -y python27 >/dev/null 2>/dev/null - -LE_AUTO_PY_34="certbot/letsencrypt-auto-source/letsencrypt-auto_py_34" -LE_AUTO="certbot/letsencrypt-auto-source/letsencrypt-auto" - -# Last version of certbot-auto that was bootstraping Python 3.4 for CentOS 6 users -INITIAL_CERTBOT_VERSION_PY34="certbot 0.38.0" - -# we're going to modify env variables, so do this in a subshell -( -# ensure CentOS6 32bits is not supported anymore, and so certbot is not installed -export UNAME_FAKE_32BITS=true -if ! "$LE_AUTO" 2>&1 | grep -q "Certbot cannot be installed."; then - echo "ERROR: certbot-auto installed certbot on 32-bit CentOS." - exit 1 -fi -) - -echo "PASSED: On CentOS 6 32 bits, certbot-auto refused to install certbot." - -# we're going to modify env variables, so do this in a subshell -( - . /opt/rh/python27/enable - - # ensure python 3 isn't installed - if python3 --version 2> /dev/null; then - echo "ERROR: Python3 is already installed." - exit 1 - fi - - # ensure python2.7 is available - if ! python2.7 --version 2> /dev/null; then - echo "ERROR: Python2.7 is not available." - exit 1 - fi - - # bootstrap, but don't install python 3. - "$LE_AUTO" --no-self-upgrade -n --version > /dev/null 2> /dev/null - - # ensure python 3 isn't installed - if python3 --version 2> /dev/null; then - echo "ERROR: letsencrypt-auto installed Python3 even though Python2.7 is present." - exit 1 - fi - - echo "PASSED: Did not upgrade to Python3 when Python2.7 is present." -) - -# ensure python2.7 isn't available -if python2.7 --version 2> /dev/null; then - echo "ERROR: Python2.7 is still available." - exit 1 -fi - -# Skip self upgrade due to Python 3 not being available. -if ! "$LE_AUTO" 2>&1 | grep -q "WARNING: couldn't find Python"; then - echo "ERROR: Python upgrade failure warning not printed!" - exit 1 -fi - -# bootstrap from the old letsencrypt-auto, this time installing python3.4 -"$LE_AUTO_PY_34" --no-self-upgrade -n --version >/dev/null 2>/dev/null - -# ensure python 3.4 is installed -if ! python3.4 --version >/dev/null 2>/dev/null; then - echo "ERROR: letsencrypt-auto failed to install Python3.4 using letsencrypt-auto < 0.37.0 when only Python2.6 is present." - exit 1 -fi - -echo "PASSED: Successfully upgraded to Python3.4 using letsencrypt-auto < 0.37.0 when only Python2.6 is present." - -# As "certbot-auto" (so without implicit --non-interactive flag set), check that the script -# refuses to install SCL Python 3.6 when run in a non interactive shell (simulated here -# using | tee /dev/null) if --non-interactive flag is not provided. -cp "$LE_AUTO" /tmp/certbot-auto -# NB: Readline has an issue on all Python versions for CentOS 6, making `certbot --version` -# output an unprintable ASCII character on a new line at the end. -# So we take the second last line of the output. -version=$(/tmp/certbot-auto --version 2>/dev/null | tee /dev/null | tail -2 | head -1) - -if [ "$version" != "$INITIAL_CERTBOT_VERSION_PY34" ]; then - echo "ERROR: certbot-auto upgraded certbot in a non-interactive shell with --non-interactive flag not set." - exit 1 -fi - -echo "PASSED: certbot-auto did not upgrade certbot in a non-interactive shell with --non-interactive flag not set." - -if [ -f /opt/rh/rh-python36/enable ]; then - echo "ERROR: certbot-auto installed Python3.6 in a non-interactive shell with --non-interactive flag not set." - exit 1 -fi - -echo "PASSED: certbot-auto did not install Python3.6 in a non-interactive shell with --non-interactive flag not set." - -# now bootstrap from current letsencrypt-auto, that will install python3.6 from SCL -"$LE_AUTO" --no-self-upgrade -n --version >/dev/null 2>/dev/null - -# Following test is executed in a subshell, to not leak any environment variable -( - # enable SCL rh-python36 - . /opt/rh/rh-python36/enable - - # ensure python 3.6 is installed - if ! python3.6 --version >/dev/null 2>/dev/null; then - echo "ERROR: letsencrypt-auto failed to install Python3.6 using current letsencrypt-auto when only Python2.6/Python3.4 are present." - exit 1 - fi - - echo "PASSED: Successfully upgraded to Python3.6 using current letsencrypt-auto when only Python2.6/Python3.4 are present." -) - -# Following test is executed in a subshell, to not leak any environment variable -( - export VENV_PATH=$(mktemp -d) - "$LE_AUTO" -n --no-bootstrap --no-self-upgrade --version >/dev/null 2>&1 - if [ "$($VENV_PATH/bin/python -V 2>&1 | cut -d" " -f2 | cut -d. -f1-2)" != "3.6" ]; then - echo "ERROR: Python 3.6 wasn't used with --no-bootstrap!" - exit 1 - fi -) - -# Following test is executed in a subshell, to not leak any environment variable -( - # enable SCL rh-python36 - . /opt/rh/rh-python36/enable - - # ensure everything works fine with certbot-auto bootstrap when python 3.6 is already enabled - export VENV_PATH=$(mktemp -d) - if ! "$LE_AUTO" --no-self-upgrade -n --version >/dev/null 2>/dev/null; then - echo "ERROR: Certbot-auto broke when Python 3.6 SCL is already enabled." - exit 1 - fi -) - -# we're going to modify env variables, so do this in a subshell -( - # ensure CentOS6 32bits is not supported anymore, and so certbot - # is not upgraded nor reinstalled. - export UNAME_FAKE_32BITS=true - OUTPUT=$("$LE_AUTO" --version 2>&1) - if ! echo "$OUTPUT" | grep -q "Certbot will no longer receive updates."; then - echo "ERROR: certbot-auto failed to run or upgraded pre-existing Certbot instance on 32-bit CentOS 6." - exit 1 - fi - if ! "$LE_AUTO" --install-only 2>&1 | grep -q "Certbot cannot be installed."; then - echo "ERROR: certbot-auto reinstalled Certbot on 32-bit CentOS 6." - exit 1 - fi -) - -# we're going to modify env variables, so do this in a subshell -( - # Prepare a certbot installation in the old venv path - rm -rf /opt/eff.org - VENV_PATH=~/.local/share/letsencrypt "$LE_AUTO" --install-only > /dev/null 2> /dev/null - # fake 32 bits mode - export UNAME_FAKE_32BITS=true - OUTPUT=$("$LE_AUTO" --version 2>&1) - if ! echo "$OUTPUT" | grep -q "Certbot will no longer receive updates."; then - echo "ERROR: certbot-auto failed to run or upgraded pre-existing Certbot instance in the old venv path on 32-bit CentOS 6." - exit 1 - fi -) - -echo "PASSED: certbot-auto refused to install/upgrade certbot on 32-bit CentOS 6." - -# test using python3 -pytest -v -s certbot/letsencrypt-auto-source/tests diff --git a/letsencrypt-auto-source/tests/certs/ca/my-root-ca.crt.pem b/letsencrypt-auto-source/tests/certs/ca/my-root-ca.crt.pem deleted file mode 100644 index 4e4d29bd225..00000000000 --- a/letsencrypt-auto-source/tests/certs/ca/my-root-ca.crt.pem +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID5jCCAs6gAwIBAgIJAI1Qkfyw88REMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV -BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRswGQYDVQQKExJNeSBCb2d1cyBS -b290IENlcnQxFDASBgNVBAMTC2V4YW1wbGUuY29tMB4XDTE1MTIwNDIwNTIxNVoX -DTQwMTIwMzIwNTIxNVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3Rh -dGUxGzAZBgNVBAoTEk15IEJvZ3VzIFJvb3QgQ2VydDEUMBIGA1UEAxMLZXhhbXBs -ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQVQpQ2EH4gTJB -NJP6+ocT3xJwT8mSXYUnvzjj6iv+JxZiXRGzAPziNzrrSRKY0yDHF+UiJwuOerLa -n8laZkLb1Ogqzs2u64rKeb0xWv90Qp+eXG0J/1xb4dw+GExqe5QFo1JUJzO/eK7m -1S04SeFkN1qV9mD5yJUy7DGiTUzDHgCxM2tXMLusXYqkxsQQ9+2EJ7BEOK4YJGEx -Sign5FuSxb64PiNow6OA97CaLl7tV4INP4w195ueDRIaS4poeOep4s8U7IAdMjIZ -EryJgKNCij50xK92vPBBJSj0NOitltBlwoEqkOZpQCOZamFd6nvt78LQ6W8Am+l6 -y6oCON5JAgMBAAGjgbgwgbUwHQYDVR0OBBYEFAlrdStDhaayLLj89Whe3Gc+HE8y -MIGFBgNVHSMEfjB8gBQJa3UrQ4Wmsiy4/PVoXtxnPhxPMqFZpFcwVTELMAkGA1UE -BhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxGzAZBgNVBAoTEk15IEJvZ3VzIFJv -b3QgQ2VydDEUMBIGA1UEAxMLZXhhbXBsZS5jb22CCQCNUJH8sPPERDAMBgNVHRME -BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQC7KAQfDTiNM3QO8Ic3x21CAPJUavkH -zshifN+Ei0+nmseHDTCTgsGfGDOToLUpUEZ4PuiHnz08UwRfd9wotc3SgY9ZaXMe -vRs8KUAF9EoyTvESzPyv2b6cS9NNMpj5y7KyXSyP17VoGbNavtiGQ4dwgEH6VgNl -0RtBvcSBv/tqxIIx1tWzL74tVEm0Kbd9BAZsYpQNKL8e6WXP35/j0PvCCvtofGrA -E8LTqMz4kCwnX+QaJIMJhBophRCsjXdAkvFbFxX0DGPztQtzIwBPcdMjsft7AFeE -0XchhDDXxw8YsbpvPfCvrD8XiiVuBycbnB1zt0LLVwB/QsCzUW9ImpLC ------END CERTIFICATE----- diff --git a/letsencrypt-auto-source/tests/certs/ca/my-root-ca.key.pem b/letsencrypt-auto-source/tests/certs/ca/my-root-ca.key.pem deleted file mode 100644 index 9caa7ddaad6..00000000000 --- a/letsencrypt-auto-source/tests/certs/ca/my-root-ca.key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA0FUKUNhB+IEyQTST+vqHE98ScE/Jkl2FJ7844+or/icWYl0R -swD84jc660kSmNMgxxflIicLjnqy2p/JWmZC29ToKs7NruuKynm9MVr/dEKfnlxt -Cf9cW+HcPhhManuUBaNSVCczv3iu5tUtOEnhZDdalfZg+ciVMuwxok1Mwx4AsTNr -VzC7rF2KpMbEEPfthCewRDiuGCRhMUooJ+RbksW+uD4jaMOjgPewmi5e7VeCDT+M -Nfebng0SGkuKaHjnqeLPFOyAHTIyGRK8iYCjQoo+dMSvdrzwQSUo9DTorZbQZcKB -KpDmaUAjmWphXep77e/C0OlvAJvpesuqAjjeSQIDAQABAoIBAH+qbVzneV3wxjwh -HUHi/p3VyHXc3xh7iNq3mwRH/1eK2nPCttLsGwwBbnC64dOXJfH7maWZKcLRPAMv -gfOM0RHn4bJB8tdrbizv91lke0DihvBDkWpb+1wvB4lh2Io0Wpwt3ojFUTfXm87G -+iQRWjbQmQlm5zyKh6uiBDSCjDTQdb9omZEBMAwlGPTZwt8TRUEtWd8QgW8FCHoB -iLER2WBwXdvn3PBtocI3VE6IYDSeZ81Xv+d7925RtVintT8Suk4toYwX+jfSz+wZ -sgHd5V6PSv9a7GUlWoUihD99D9wqDZE8IvMDZ5ofSAUd1KfICDtmsEyugY7u2yYZ -tYt49AECgYEA73f7ITMHg8JsUipqb6eG10gCRtRhkqrrO1g/TNeTBh3CTrQGb56e -y6kmUivn5gK46t3T2N4Ht4IR8fpLcJcbPYPQNulSjmWm5y6WduafXW/VCW1NA9Lc -FyGPkMxFCIVJTLFxfLFepBVvtUzLLDKGGtQxru/GNbBzjdtmVfDPIoECgYEA3rbM -cTfvj+jWrV1YsRbphyjy+k3OJEIVx6KA4s5d7Tp12UfYQp/B3HPhXXm5wqeo1Nos -UAEWZIMi1VoE8iu6jjeJ6uERtbKKQVed25Us/ff0jUPbxlXgiBOtRcllq9d9Srjm -ybHUgfjLsZ2/xpIcOl+oI5pDM9JvD8Sq4ZCFR8kCgYBK/H0tFjeiML2OtS2DLShy -PWBJIbQ0I0Vp3eZkf5TQc30m/ASP61G6YItZa9pAElYpZbEy1cQA2MAZz9DTvt2O -07ndmA57/KTY+6OuM+Vvctd5DjrxmZPFwoKcSvrLAkHDvETXUQtbwkKquRNeEawg -tpWgPAELSufEYhGXk8KpAQKBgBDCqPgMQZcO6rj5QWdyVfi5+C8mE9Fet8ziSdjH -twHXWG8VnQzGgQxaHCewtW4Ut/vsv1D2A/1kcQalU6H18IArZdGrRm3qFcV9FoAj -5dLnChxncu6mH9Odx3htA52/BcrNx3B+VYPCeXHQcVI8RKuP71NelJgdygXhwwpe -mekhAoGBAOUovnqylciYa9HRqo+xZk59eyX+ehhnlV8SeJ2K0PwaQkzQ0KYtCmE7 -kdSdhcv8h/IQKGaFfc/LyFMM/a26PfAeY5bj41UjkT0K5hQrYuL/52xaT401YLcb -Xo+bZz9K0hrdP7TdZFuTY/WxojXgjsVAuAN1NwnJumqxhzPh+hfl ------END RSA PRIVATE KEY----- diff --git a/letsencrypt-auto-source/tests/certs/ca/my-root-ca.srl b/letsencrypt-auto-source/tests/certs/ca/my-root-ca.srl deleted file mode 100644 index ad6d262b459..00000000000 --- a/letsencrypt-auto-source/tests/certs/ca/my-root-ca.srl +++ /dev/null @@ -1 +0,0 @@ -D613482D0EF95DD0 diff --git a/letsencrypt-auto-source/tests/certs/localhost/cert.pem b/letsencrypt-auto-source/tests/certs/localhost/cert.pem deleted file mode 100644 index ac83535ced6..00000000000 --- a/letsencrypt-auto-source/tests/certs/localhost/cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDKjCCAhICCQDWE0gtDvld0DANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJB -VTETMBEGA1UECBMKU29tZS1TdGF0ZTEbMBkGA1UEChMSTXkgQm9ndXMgUm9vdCBD -ZXJ0MRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNTEyMDQyMDU0MzFaFw00MDEy -MDMyMDU0MzFaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEw -HwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2Fs -aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2WIIi86Mis4UQH -a5PrFbX2PBtQHbI3t3ekN1CewRsgQ/2X3lCeWhKmr3CJYXVgA7q/23PORQAiuV6y -DG2dQIrjeahWCXaCptTi49ljfVRTW2IxrHke/iA8TkDuZbWGzVLb8TB83ipBOD41 -SjuomoN4A/ktnIfbNqRqgjjHs2wwJHDfxPiCQlwyOayjHmdlh8cqfVE8rWEm5/3T -Iu0X1J53SammR1SbUmsLJNofxFYMK1ogHb0CaFEG9QuuUDPJl5K74Rr6InMQZKPn -ne4W3cGoALxPHAca7yicpSMSmdsmd6pqylc2Fdua7o/wf0SwShxS4A1DqA/HWLEM -V6MSEF8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAz5sMAFG6W/ZEULZITkBTCU6P -NttpGiKufnqyBW5HyNylaczfnHnClvQjr8f/84xvKVcfC3xP0lz+92aIQqo+5L/n -v7gLhBFR4Vr2XwMt2qz2FpkaxmVwnhVAHaaC05WIKQ6W2gDwWT0u1K8YdTh+7mvN -AT9FW4vDgtNZWq4W/PePh9QCiOOQhGOuBYj/7zqLtz4XPifhi66ILIRDHiu0kond -3YMFcECIAf4MPT9vT0iNcWX+c8CfAixPt8nMD6bzOo3oTcfuZh/2enfgLbMqOlOi -uk72FM5VVPXTWAckJvL/vVjqsvDuJQKqbr0oUc3bdWbS36xtWZUycp4IQLguAQ== ------END CERTIFICATE----- diff --git a/letsencrypt-auto-source/tests/certs/localhost/localhost.csr.pem b/letsencrypt-auto-source/tests/certs/localhost/localhost.csr.pem deleted file mode 100644 index 8a6189f8841..00000000000 --- a/letsencrypt-auto-source/tests/certs/localhost/localhost.csr.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICnjCCAYYCAQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx -ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAxMJbG9j -YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZYgiLzoyKzh -RAdrk+sVtfY8G1Adsje3d6Q3UJ7BGyBD/ZfeUJ5aEqavcIlhdWADur/bc85FACK5 -XrIMbZ1AiuN5qFYJdoKm1OLj2WN9VFNbYjGseR7+IDxOQO5ltYbNUtvxMHzeKkE4 -PjVKO6iag3gD+S2ch9s2pGqCOMezbDAkcN/E+IJCXDI5rKMeZ2WHxyp9UTytYSbn -/dMi7RfUnndJqaZHVJtSawsk2h/EVgwrWiAdvQJoUQb1C65QM8mXkrvhGvoicxBk -o+ed7hbdwagAvE8cBxrvKJylIxKZ2yZ3qmrKVzYV25ruj/B/RLBKHFLgDUOoD8dY -sQxXoxIQXwIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAFbg3WrAokoPx7iAYG6z -PqeDd4/XanXjeL4Ryxv6LoGhu69mmBAd3N5ILPyQJjnkWpIjEmJDzEcPMzhQjRh5 -GlWTyvKWO4zClYU840KZk7crVkpzNZ+HP0YeM/Agz6sab00ffRcq5m1wEF9MCvDE -8FUXk1HBHRAb/6t9QV/7axsPOkGT8SjQ1v2SCaiB0HQL3sYChYLi5zu4dfmQNPGq -ar9Xm5a0YqOQIFfmy8RSwxk0Q/ipNFTGN1uvlIRkgbT9zPnodxjWZsSI9BF+q5Af -uiE/oAk7MxfJ0LyLfhOWB+T98bKIOVtFT3wMLS1IIgMogwqCEXFf30Q9p2iTEzqT -6UE= ------END CERTIFICATE REQUEST----- diff --git a/letsencrypt-auto-source/tests/certs/localhost/privkey.pem b/letsencrypt-auto-source/tests/certs/localhost/privkey.pem deleted file mode 100644 index 18feba40363..00000000000 --- a/letsencrypt-auto-source/tests/certs/localhost/privkey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEArZYgiLzoyKzhRAdrk+sVtfY8G1Adsje3d6Q3UJ7BGyBD/Zfe -UJ5aEqavcIlhdWADur/bc85FACK5XrIMbZ1AiuN5qFYJdoKm1OLj2WN9VFNbYjGs -eR7+IDxOQO5ltYbNUtvxMHzeKkE4PjVKO6iag3gD+S2ch9s2pGqCOMezbDAkcN/E -+IJCXDI5rKMeZ2WHxyp9UTytYSbn/dMi7RfUnndJqaZHVJtSawsk2h/EVgwrWiAd -vQJoUQb1C65QM8mXkrvhGvoicxBko+ed7hbdwagAvE8cBxrvKJylIxKZ2yZ3qmrK -VzYV25ruj/B/RLBKHFLgDUOoD8dYsQxXoxIQXwIDAQABAoIBAG8bVJ+xKt6nqVg9 -16HKKw9ZGIfy888K0qgFuFImCzwtntdGycmYUdb2Uf0aMgNK/ZgfDXxGXuwDTdtK -46GVsaY0i74vs8bjQZ2pzGVsxN+gqzFi0h6Es+w2LXBqJzfVnL6YgPykMB+jtzg6 -K9Wbyaq0uvZXN4XNzl/WvJtTV4i7Cff1MOd5EhKFdqxrZvB/SRBCr/SMMafRtB9P -EvMneNKzhmlrutHAxuyxEKZR32Kkx7ydAdTjGgn+rE+NL5BweXfeWhLU4Bv14bn9 -Mkneu3w5o1ryJfE2YnVajUP//jeopUT0nTQ3MpEusBQCLBlvFXjjM9uCaFX+5+MP -0H4xVcECgYEA1Q+wR3GHbk37vIGSlbENyUsri5WlMt8IVAHsDsTOpxAjYB0yyo+x -h9RS+RJZQECJlA6H72peUl3GM7RgdWIcKOT3nZ12XqYKG57rr/N5zlUuxbdS8KBk -JhyZeJdYjq/Jrno1ZP+OSmc7VvBLcM7irY7LHlvK0o8W1W0TNJ8jrZkCgYEA0JHX -lJd+fiezcUS7g4moHtzJp0JKquQiXLX+c2urmpyhb3ZrTuQ8OUjSy6DlwHlgDx8K -Hg2sdx/ZCuDaGjR4IY/Qs5RFt9WUqlK9gi9V3nYVrzBOQkdFOf/Ad3j4pQ8/aeCX -nP6snHXz1WqPpbCXG6l6GzFGbQU473GfuKsDuLcCgYAWQaNKc0OQdDj9whNL68ji -5CVSWXl+TOoTzHeaO1jS/s6TNbmei1AiPj3EovQL0DIO802j5tqfhAg2UntZB7yl -UPXE0zQQQwv/QqSgJrDsqt1N7g6N8FNF3+rwO+8WSKqqvT1ipYd5ojsCo+tdh18K -fkYdj70qLaRW+yPsdUtG0QKBgEYc8NqbvsML94+ZKmwCh4iwcf2PFGi0PjTqXTpR -tKNKCh7dMR+ZLAGZ0HrxgKqeYsNSjOUjdZmqFB1LDyaGAuhNXzwvGOy+mLZVEC3G -Wdhp28pDs9sl+EiSCBJhkTxzjr656F23YzFJmYlhxB5P6cw7wbeIbgNSIRylFqtO -mfarAoGBAICsAEWypOctxtmtOcjxgJ7jMbOA7rrsGlXpiy1/WlwIwRGF5LMvIIFX -qFAfiPcZn05ZgdAGzaFYowdjmQB10FW0jZbDf+nIHfOF5YmfmfWjsaweEGALJmqB -okGu/lGNGf3XoYzy0/hC3WAqk3znSZtQLUq8jEWF7dLNUizUeUow ------END RSA PRIVATE KEY----- diff --git a/letsencrypt-auto-source/tests/certs/localhost/server.pem b/letsencrypt-auto-source/tests/certs/localhost/server.pem deleted file mode 100644 index c5765dd890d..00000000000 --- a/letsencrypt-auto-source/tests/certs/localhost/server.pem +++ /dev/null @@ -1,46 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEArZYgiLzoyKzhRAdrk+sVtfY8G1Adsje3d6Q3UJ7BGyBD/Zfe -UJ5aEqavcIlhdWADur/bc85FACK5XrIMbZ1AiuN5qFYJdoKm1OLj2WN9VFNbYjGs -eR7+IDxOQO5ltYbNUtvxMHzeKkE4PjVKO6iag3gD+S2ch9s2pGqCOMezbDAkcN/E -+IJCXDI5rKMeZ2WHxyp9UTytYSbn/dMi7RfUnndJqaZHVJtSawsk2h/EVgwrWiAd -vQJoUQb1C65QM8mXkrvhGvoicxBko+ed7hbdwagAvE8cBxrvKJylIxKZ2yZ3qmrK -VzYV25ruj/B/RLBKHFLgDUOoD8dYsQxXoxIQXwIDAQABAoIBAG8bVJ+xKt6nqVg9 -16HKKw9ZGIfy888K0qgFuFImCzwtntdGycmYUdb2Uf0aMgNK/ZgfDXxGXuwDTdtK -46GVsaY0i74vs8bjQZ2pzGVsxN+gqzFi0h6Es+w2LXBqJzfVnL6YgPykMB+jtzg6 -K9Wbyaq0uvZXN4XNzl/WvJtTV4i7Cff1MOd5EhKFdqxrZvB/SRBCr/SMMafRtB9P -EvMneNKzhmlrutHAxuyxEKZR32Kkx7ydAdTjGgn+rE+NL5BweXfeWhLU4Bv14bn9 -Mkneu3w5o1ryJfE2YnVajUP//jeopUT0nTQ3MpEusBQCLBlvFXjjM9uCaFX+5+MP -0H4xVcECgYEA1Q+wR3GHbk37vIGSlbENyUsri5WlMt8IVAHsDsTOpxAjYB0yyo+x -h9RS+RJZQECJlA6H72peUl3GM7RgdWIcKOT3nZ12XqYKG57rr/N5zlUuxbdS8KBk -JhyZeJdYjq/Jrno1ZP+OSmc7VvBLcM7irY7LHlvK0o8W1W0TNJ8jrZkCgYEA0JHX -lJd+fiezcUS7g4moHtzJp0JKquQiXLX+c2urmpyhb3ZrTuQ8OUjSy6DlwHlgDx8K -Hg2sdx/ZCuDaGjR4IY/Qs5RFt9WUqlK9gi9V3nYVrzBOQkdFOf/Ad3j4pQ8/aeCX -nP6snHXz1WqPpbCXG6l6GzFGbQU473GfuKsDuLcCgYAWQaNKc0OQdDj9whNL68ji -5CVSWXl+TOoTzHeaO1jS/s6TNbmei1AiPj3EovQL0DIO802j5tqfhAg2UntZB7yl -UPXE0zQQQwv/QqSgJrDsqt1N7g6N8FNF3+rwO+8WSKqqvT1ipYd5ojsCo+tdh18K -fkYdj70qLaRW+yPsdUtG0QKBgEYc8NqbvsML94+ZKmwCh4iwcf2PFGi0PjTqXTpR -tKNKCh7dMR+ZLAGZ0HrxgKqeYsNSjOUjdZmqFB1LDyaGAuhNXzwvGOy+mLZVEC3G -Wdhp28pDs9sl+EiSCBJhkTxzjr656F23YzFJmYlhxB5P6cw7wbeIbgNSIRylFqtO -mfarAoGBAICsAEWypOctxtmtOcjxgJ7jMbOA7rrsGlXpiy1/WlwIwRGF5LMvIIFX -qFAfiPcZn05ZgdAGzaFYowdjmQB10FW0jZbDf+nIHfOF5YmfmfWjsaweEGALJmqB -okGu/lGNGf3XoYzy0/hC3WAqk3znSZtQLUq8jEWF7dLNUizUeUow ------END RSA PRIVATE KEY----- ------BEGIN CERTIFICATE----- -MIIDKjCCAhICCQDWE0gtDvld0DANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJB -VTETMBEGA1UECBMKU29tZS1TdGF0ZTEbMBkGA1UEChMSTXkgQm9ndXMgUm9vdCBD -ZXJ0MRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNTEyMDQyMDU0MzFaFw00MDEy -MDMyMDU0MzFaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEw -HwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2Fs -aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2WIIi86Mis4UQH -a5PrFbX2PBtQHbI3t3ekN1CewRsgQ/2X3lCeWhKmr3CJYXVgA7q/23PORQAiuV6y -DG2dQIrjeahWCXaCptTi49ljfVRTW2IxrHke/iA8TkDuZbWGzVLb8TB83ipBOD41 -SjuomoN4A/ktnIfbNqRqgjjHs2wwJHDfxPiCQlwyOayjHmdlh8cqfVE8rWEm5/3T -Iu0X1J53SammR1SbUmsLJNofxFYMK1ogHb0CaFEG9QuuUDPJl5K74Rr6InMQZKPn -ne4W3cGoALxPHAca7yicpSMSmdsmd6pqylc2Fdua7o/wf0SwShxS4A1DqA/HWLEM -V6MSEF8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAz5sMAFG6W/ZEULZITkBTCU6P -NttpGiKufnqyBW5HyNylaczfnHnClvQjr8f/84xvKVcfC3xP0lz+92aIQqo+5L/n -v7gLhBFR4Vr2XwMt2qz2FpkaxmVwnhVAHaaC05WIKQ6W2gDwWT0u1K8YdTh+7mvN -AT9FW4vDgtNZWq4W/PePh9QCiOOQhGOuBYj/7zqLtz4XPifhi66ILIRDHiu0kond -3YMFcECIAf4MPT9vT0iNcWX+c8CfAixPt8nMD6bzOo3oTcfuZh/2enfgLbMqOlOi -uk72FM5VVPXTWAckJvL/vVjqsvDuJQKqbr0oUc3bdWbS36xtWZUycp4IQLguAQ== ------END CERTIFICATE----- diff --git a/letsencrypt-auto-source/tests/fake-letsencrypt/dist/letsencrypt-99.9.9.tar.gz b/letsencrypt-auto-source/tests/fake-letsencrypt/dist/letsencrypt-99.9.9.tar.gz deleted file mode 100644 index 5f9a48a34a2..00000000000 Binary files a/letsencrypt-auto-source/tests/fake-letsencrypt/dist/letsencrypt-99.9.9.tar.gz and /dev/null differ diff --git a/letsencrypt-auto-source/tests/fake-letsencrypt/letsencrypt.py b/letsencrypt-auto-source/tests/fake-letsencrypt/letsencrypt.py deleted file mode 100755 index 9d811fab527..00000000000 --- a/letsencrypt-auto-source/tests/fake-letsencrypt/letsencrypt.py +++ /dev/null @@ -1,8 +0,0 @@ -from sys import argv, stderr - - -def main(): - """Act like letsencrypt --version insofar as printing the version number to - stderr.""" - if '--version' in argv: - stderr.write('letsencrypt 99.9.9\n') diff --git a/letsencrypt-auto-source/tests/fake-letsencrypt/setup.py b/letsencrypt-auto-source/tests/fake-letsencrypt/setup.py deleted file mode 100644 index e5f7fde350e..00000000000 --- a/letsencrypt-auto-source/tests/fake-letsencrypt/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -from setuptools import setup - - -setup( - name='letsencrypt', - version='99.9.9', - description='A mock version of letsencrypt that just prints its version', - py_modules=['letsencrypt'], - entry_points={ - 'console_scripts': ['letsencrypt = letsencrypt:main'] - } -) diff --git a/letsencrypt-auto-source/tests/oraclelinux6_tests.sh b/letsencrypt-auto-source/tests/oraclelinux6_tests.sh deleted file mode 100644 index f3fd952f305..00000000000 --- a/letsencrypt-auto-source/tests/oraclelinux6_tests.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash -set -eo pipefail -# Start by making sure your system is up-to-date: -yum update -y >/dev/null - -LE_AUTO_PY_34="certbot/letsencrypt-auto-source/letsencrypt-auto_py_34" -LE_AUTO="certbot/letsencrypt-auto-source/letsencrypt-auto" - -# Apply installation instructions from official documentation: -# https://certbot.eff.org/lets-encrypt/centosrhel6-other -cp "$LE_AUTO" /usr/local/bin/certbot-auto -chown root /usr/local/bin/certbot-auto -chmod 0755 /usr/local/bin/certbot-auto -LE_AUTO=/usr/local/bin/certbot-auto - -# Last version of certbot-auto that was bootstraping Python 3.4 for CentOS 6 users -INITIAL_CERTBOT_VERSION_PY34="certbot 0.38.0" - -# Check bootstrap from current certbot-auto will fail, because SCL is not enabled. -set +o pipefail -if ! "$LE_AUTO" -n 2>&1 | grep -q "Enable the SCL repository and try running Certbot again."; then - echo "ERROR: Bootstrap was not aborted although SCL was not installed!" - exit 1 -fi -set -o pipefail - -echo "PASSED: Bootstrap was aborted since SCL was not installed." - -# Bootstrap from the old letsencrypt-auto, Python 3.4 will be installed from EPEL. -"$LE_AUTO_PY_34" --no-self-upgrade -n --install-only >/dev/null 2>/dev/null - -# Ensure Python 3.4 is installed -if ! command -v python3.4 &>/dev/null; then - echo "ERROR: old letsencrypt-auto failed to install Python3.4 using letsencrypt-auto < 0.37.0 when only Python2.6 is present." - exit 1 -fi - -echo "PASSED: Bootstrap from old letsencrypt-auto succeeded and installed Python 3.4" - -# Expect certbot-auto to skip rebootstrapping with a warning since SCL is not installed. -if ! "$LE_AUTO" --non-interactive --version 2>&1 | grep -q "This requires manual user intervention"; then - echo "FAILED: Script certbot-auto did not print a warning about needing manual intervention!" - exit 1 -fi - -echo "PASSED: Script certbot-auto did not rebootstrap." - -# NB: Readline has an issue on all Python versions for OL 6, making `certbot --version` -# output an unprintable ASCII character on a new line at the end. -# So we take the second last line of the output. -version=$($LE_AUTO --version 2>/dev/null | tail -2 | head -1) - -if [ "$version" != "$INITIAL_CERTBOT_VERSION_PY34" ]; then - echo "ERROR: Script certbot-auto upgraded certbot in a non-interactive shell while SCL was not enabled." - exit 1 -fi - -echo "PASSED: Script certbot-auto did not upgrade certbot but started it successfully while SCL was not enabled." - -# Enable SCL -yum install -y oracle-softwarecollection-release-el6 >/dev/null - -# Expect certbot-auto to bootstrap successfully since SCL is available. -"$LE_AUTO" -n --version &>/dev/null - -if [ "$(/opt/eff.org/certbot/venv/bin/python -V 2>&1 | cut -d" " -f2 | cut -d. -f1-2)" != "3.6" ]; then - echo "ERROR: Script certbot-auto failed to bootstrap and install Python 3.6 while SCL is available." - exit 1 -fi - -if ! /opt/eff.org/certbot/venv/bin/certbot --version > /dev/null 2> /dev/null; then - echo "ERROR: Script certbot-auto did not install certbot correctly while SCL is enabled." - exit 1 -fi - -echo "PASSED: Script certbot-auto correctly bootstraped Certbot using rh-python36 when SCL is available." - -# Expect certbot-auto will be totally silent now that everything has been correctly boostraped. -OUTPUT_LEN=$("$LE_AUTO" --install-only --no-self-upgrade --quiet 2>&1 | wc -c) -if [ "$OUTPUT_LEN" != 0 ]; then - echo certbot-auto produced unexpected output! - exit 1 -fi - -echo "PASSED: Script certbot-auto did not print anything in quiet mode." diff --git a/letsencrypt-auto-source/tests/uname_wrapper.sh b/letsencrypt-auto-source/tests/uname_wrapper.sh deleted file mode 100644 index df1f568c654..00000000000 --- a/letsencrypt-auto-source/tests/uname_wrapper.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -e - -uname_output=$(/bin/uname_orig "$@") - -if [ "$UNAME_FAKE_32BITS" = true ]; then - uname_output="${uname_output//x86_64/i686}" -fi - -echo "$uname_output" diff --git a/linter_plugin.py b/linter_plugin.py index 75879f73acb..a19bf7df9b5 100644 --- a/linter_plugin.py +++ b/linter_plugin.py @@ -10,7 +10,9 @@ from pylint.interfaces import IAstroidChecker # Modules in theses packages can import the os module. -WHITELIST_PACKAGES = ['acme', 'certbot_compatibility_test', 'lock_test'] +WHITELIST_PACKAGES = [ + 'acme', 'certbot_integration_tests', 'certbot_compatibility_test', 'lock_test' +] class ForbidStandardOsModule(BaseChecker): @@ -25,8 +27,8 @@ class ForbidStandardOsModule(BaseChecker): 'E5001': ( 'Forbidden use of os module, certbot.compat.os must be used instead', 'os-module-forbidden', - 'Some methods from the standard os module cannot be used for security reasons on Windows: ' - 'the safe wrapper certbot.compat.os must be used instead in Certbot.' + 'Some methods from the standard os module cannot be used for security reasons on ' + 'Windows: the safe wrapper certbot.compat.os must be used instead in Certbot.' ) } priority = -1 diff --git a/pull_request_template.md b/pull_request_template.md index c806d33e8f1..53298291bf9 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,4 +1,5 @@ ## Pull Request Checklist - [ ] If the change being made is to a [distributed component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout), edit the `master` section of `certbot/CHANGELOG.md` to include a description of the change being made. +- [ ] Add or update any documentation as needed to support the changes in this PR. - [ ] Include your name in `AUTHORS.md` if you like. diff --git a/pytest.ini b/pytest.ini index 16aa9a193f1..b7a6928ea76 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,6 +4,8 @@ [pytest] # In general, all warnings are treated as errors. Here are the exceptions: # 1- decodestring: https://github.com/rthalley/dnspython/issues/338 +# 2- Python 2 deprecation: https://github.com/certbot/certbot/issues/8388 +# (to be removed with Certbot 1.12.0 and its drop of Python 2 support) # Warnings being triggered by our plugins using deprecated features in # acme/certbot should be fixed by having our plugins no longer using the # deprecated code rather than adding them to the list of ignored warnings here. @@ -14,3 +16,4 @@ filterwarnings = error ignore:decodestring:DeprecationWarning + ignore:Python 2 support will be dropped:PendingDeprecationWarning diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 5fbf8503da7..c9061ecb31f 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -20,13 +20,13 @@ adopt-info: certbot apps: certbot: - command: bin/python3 $SNAP/bin/certbot + command: bin/python3 -s $SNAP/bin/certbot environment: PATH: "$SNAP/bin:$SNAP/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games" AUGEAS_LENS_LIB: "$SNAP/usr/share/augeas/lenses/dist" CERTBOT_SNAPPED: "True" renew: - command: bin/python3 $SNAP/bin/certbot -q renew + command: bin/python3 -s $SNAP/bin/certbot -q renew daemon: oneshot environment: PATH: "$SNAP/bin:$SNAP/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games" @@ -40,7 +40,6 @@ parts: certbot: plugin: python source: . - constraints: [$SNAPCRAFT_PART_SRC/snap-constraints.txt] python-packages: - git+https://github.com/certbot/python-augeas.git@certbot-patched - ./acme @@ -64,7 +63,6 @@ parts: - libpython3.8-stdlib - libpython3.8-minimal - python3-pip - - python3-setuptools - python3-wheel - python3-venv - python3-minimal @@ -74,13 +72,23 @@ parts: # To build cryptography and cffi if needed build-packages: [gcc, libffi-dev, libssl-dev, git, libaugeas-dev, python3-dev] build-environment: - - SNAPCRAFT_PYTHON_VENV_ARGS: --system-site-packages - - PIP_NO_BUILD_ISOLATION: "no" + - SNAPCRAFT_PYTHON_VENV_ARGS: --upgrade + # Constraints are passed through the environment variable PIP_CONSTRAINTS instead of using the + # parts.[part_name].constraints option available in snapcraft.yaml when the Python plugin is + # used. This is done to let these constraints be applied not only on the certbot package + # build, but also on any isolated build that pip could trigger when building wheels for + # dependencies. See https://github.com/certbot/certbot/pull/8443 for more info. + - PIP_CONSTRAINT: $SNAPCRAFT_PART_SRC/snap-constraints.txt + override-build: | + python3 -m venv "${SNAPCRAFT_PART_INSTALL}" + "${SNAPCRAFT_PART_INSTALL}/bin/python3" "${SNAPCRAFT_PART_SRC}/tools/pipstrap.py" + snapcraftctl build override-pull: | - snapcraftctl pull - cd $SNAPCRAFT_PART_SRC - python3 tools/strip_hashes.py letsencrypt-auto-source/pieces/dependency-requirements.txt | grep -v python-augeas > snap-constraints.txt - snapcraftctl set-version `grep -oP "__version__ = '\K.*(?=')" $SNAPCRAFT_PART_SRC/certbot/certbot/__init__.py` + snapcraftctl pull + python3 "${SNAPCRAFT_PART_SRC}/tools/strip_hashes.py" "${SNAPCRAFT_PART_SRC}/letsencrypt-auto-source/pieces/dependency-requirements.txt" | grep -v python-augeas >> "${SNAPCRAFT_PART_SRC}/snap-constraints.txt" + python3 "${SNAPCRAFT_PART_SRC}/tools/strip_hashes.py" "${SNAPCRAFT_PART_SRC}/tools/pipstrap_constraints.txt" >> "${SNAPCRAFT_PART_SRC}/snap-constraints.txt" + echo "$(python3 "${SNAPCRAFT_PART_SRC}/tools/merge_requirements.py" "${SNAPCRAFT_PART_SRC}/snap-constraints.txt")" > "${SNAPCRAFT_PART_SRC}/snap-constraints.txt" + snapcraftctl set-version `grep -oP "__version__ = '\K.*(?=')" "${SNAPCRAFT_PART_SRC}/certbot/certbot/__init__.py"` shared-metadata: plugin: dump source: . diff --git a/tests/letstest/auto_targets.yaml b/tests/letstest/auto_targets.yaml index 76b3a3dc552..01d410227d1 100644 --- a/tests/letstest/auto_targets.yaml +++ b/tests/letstest/auto_targets.yaml @@ -31,10 +31,6 @@ targets: virt: hvm user: admin machine_type: a1.medium - # userdata: | - # #cloud-init - # runcmd: - # - [ apt-get, install, -y, curl ] #----------------------------------------------------------------------------- # Other Redhat Distros - ami: ami-0916c408cb02e310b @@ -56,17 +52,6 @@ targets: type: centos virt: hvm user: centos - # centos6 requires EPEL repo added - - ami: ami-1585c46a - name: centos6 - type: centos - virt: hvm - user: centos - userdata: | - #cloud-config - runcmd: - - yum install -y epel-release - - iptables -F - ami: ami-01ca03df4a6012157 name: centos8 type: centos diff --git a/tests/letstest/multitester.py b/tests/letstest/multitester.py index cf9f2899ad7..1a1958bd2e3 100644 --- a/tests/letstest/multitester.py +++ b/tests/letstest/multitester.py @@ -147,22 +147,32 @@ def make_instance(ec2_client, keyname, security_group_id, subnet_id, - machine_type='t2.micro', - userdata=""): #userdata contains bash or cloud-init script + self_destruct, + machine_type='t2.micro'): + """Creates an instance using the given parameters. + + If self_destruct is True, the instance will be configured to shutdown after + 1 hour and to terminate itself on shutdown. + + """ block_device_mappings = _get_block_device_mappings(ec2_client, ami_id) tags = [{'Key': 'Name', 'Value': instance_name}] tag_spec = [{'ResourceType': 'instance', 'Tags': tags}] - return ec2_client.create_instances( - BlockDeviceMappings=block_device_mappings, - ImageId=ami_id, - SecurityGroupIds=[security_group_id], - SubnetId=subnet_id, - KeyName=keyname, - MinCount=1, - MaxCount=1, - UserData=userdata, - InstanceType=machine_type, - TagSpecifications=tag_spec)[0] + kwargs = { + 'BlockDeviceMappings': block_device_mappings, + 'ImageId': ami_id, + 'SecurityGroupIds': [security_group_id], + 'SubnetId': subnet_id, + 'KeyName': keyname, + 'MinCount': 1, + 'MaxCount': 1, + 'InstanceType': machine_type, + 'TagSpecifications': tag_spec + } + if self_destruct: + kwargs['InstanceInitiatedShutdownBehavior'] = 'terminate' + kwargs['UserData'] = '#!/bin/bash\nshutdown -P +60\n' + return ec2_client.create_instances(**kwargs)[0] def _get_block_device_mappings(ec2_client, ami_id): """Returns the list of block device mappings to ensure cleanup. @@ -313,7 +323,7 @@ def grab_certbot_log(cxn): 'cat ./certbot.log; else echo "[nolocallog]"; fi\'') -def create_client_instance(ec2_client, target, security_group_id, subnet_id): +def create_client_instance(ec2_client, target, security_group_id, subnet_id, self_destruct): """Create a single client instance for running tests.""" if 'machine_type' in target: machine_type = target['machine_type'] @@ -322,10 +332,6 @@ def create_client_instance(ec2_client, target, security_group_id, subnet_id): else: # 32 bit systems machine_type = 'c1.medium' - if 'userdata' in target: - userdata = target['userdata'] - else: - userdata = '' name = 'le-%s'%target['name'] print(name, end=" ") return make_instance(ec2_client, @@ -335,7 +341,7 @@ def create_client_instance(ec2_client, target, security_group_id, subnet_id): machine_type=machine_type, security_group_id=security_group_id, subnet_id=subnet_id, - userdata=userdata) + self_destruct=self_destruct) def test_client_process(fab_config, inqueue, outqueue, boulder_url, log_dir): @@ -490,6 +496,9 @@ def main(): boulder_preexists = True else: print("Can't find a boulder server, starting one...") + # If we want to kill boulder on shutdown, have it self-destruct in case + # cleanup fails. + self_destruct = cl_args.killboulder boulder_server = make_instance(ec2_client, 'le-boulderserver', BOULDER_AMI, @@ -497,16 +506,20 @@ def main(): machine_type='t2.micro', #machine_type='t2.medium', security_group_id=security_group_id, - subnet_id=subnet_id) + subnet_id=subnet_id, + self_destruct=self_destruct) instances = [] try: if not cl_args.boulderonly: print("Creating instances: ", end="") + # If we want to preserve instances, do not have them self-destruct. + self_destruct = not cl_args.saveinstances for target in targetlist: instances.append( create_client_instance(ec2_client, target, - security_group_id, subnet_id) + security_group_id, subnet_id, + self_destruct) ) print() diff --git a/tests/letstest/scripts/bootstrap_os_packages.sh b/tests/letstest/scripts/bootstrap_os_packages.sh index 96506282b45..7ad93f63e12 100755 --- a/tests/letstest/scripts/bootstrap_os_packages.sh +++ b/tests/letstest/scripts/bootstrap_os_packages.sh @@ -98,41 +98,6 @@ BootstrapRpmCommonBase() { fi } -# This bootstrap concerns old RedHat-based distributions that do not ship by default -# with Python 2.7, but only Python 2.6. We bootstrap them by enabling SCL and installing -# Python 3.6. Some of these distributions are: CentOS/RHEL/OL/SL 6. -BootstrapRpmPython3Legacy() { - # Tested with: - # - CentOS 6 - - InitializeRPMCommonBase - - if ! "${TOOL}" list rh-python36 >/dev/null 2>&1; then - echo "To use Certbot on this operating system, packages from the SCL repository need to be installed." - if ! "${TOOL}" list centos-release-scl >/dev/null 2>&1; then - error "Enable the SCL repository and try running Certbot again." - exit 1 - fi - if ! "${TOOL}" install -y centos-release-scl; then - error "Could not enable SCL. Aborting bootstrap!" - exit 1 - fi - fi - - # CentOS 6 must use rh-python36 from SCL - if "${TOOL}" list rh-python36 >/dev/null 2>&1; then - python_pkgs="rh-python36-python - rh-python36-python-virtualenv - rh-python36-python-devel - " - else - error "No supported Python package available to install. Aborting bootstrap!" - exit 1 - fi - - BootstrapRpmCommonBase "${python_pkgs}" -} - BootstrapRpmPython3() { InitializeRPMCommonBase @@ -154,16 +119,9 @@ if [ -f /etc/debian_version ]; then } elif [ -f /etc/redhat-release ]; then DeterminePythonVersion - # Handle legacy RPM distributions - if [ "$PYVER" -eq 26 ]; then - Bootstrap() { - BootstrapRpmPython3Legacy - } - else - Bootstrap() { - BootstrapRpmPython3 - } - fi + Bootstrap() { + BootstrapRpmPython3 + } fi diff --git a/tests/letstest/scripts/test_apache2.sh b/tests/letstest/scripts/test_apache2.sh index ba3d9437920..c7f9260566a 100755 --- a/tests/letstest/scripts/test_apache2.sh +++ b/tests/letstest/scripts/test_apache2.sh @@ -64,12 +64,6 @@ if [ $? -ne 0 ] ; then exit 1 fi -if command -v python && [ $(python -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//') -eq 26 ]; then - # RHEL/CentOS 6 will need a special treatment, so we need to detect that environment - # Enable the SCL Python 3.6 installed by letsencrypt-auto bootstrap - PATH="/opt/rh/rh-python36/root/usr/bin:$PATH" -fi - tools/venv3.py -e acme[dev] -e certbot[dev,docs] -e certbot-apache sudo "venv3/bin/certbot" -v --debug --text --agree-tos \ diff --git a/tests/letstest/scripts/test_leauto_upgrades.sh b/tests/letstest/scripts/test_leauto_upgrades.sh index 51ff640c520..1eeafad2107 100755 --- a/tests/letstest/scripts/test_leauto_upgrades.sh +++ b/tests/letstest/scripts/test_leauto_upgrades.sh @@ -105,15 +105,10 @@ if ./letsencrypt-auto -v --debug --version | grep "WARNING: couldn't find Python exit 1 fi -# On systems like Debian where certbot-auto is deprecated, we expect it to -# leave existing Certbot installations unmodified so we check for the same -# version that was initially installed below. Once certbot-auto is deprecated -# on RHEL systems, we can unconditionally check for INITIAL_VERSION. -if [ -f /etc/debian_version ]; then - EXPECTED_VERSION="$INITIAL_VERSION" -else - EXPECTED_VERSION=$(grep -m1 LE_AUTO_VERSION certbot-auto | cut -d\" -f2) -fi +# Since certbot-auto is deprecated, we expect it to leave existing Certbot +# installations unmodified so we check for the same version that was initially +# installed below. +EXPECTED_VERSION="$INITIAL_VERSION" if ! /opt/eff.org/certbot/venv/bin/letsencrypt --version 2>&1 | tail -n1 | grep "^certbot $EXPECTED_VERSION$" ; then echo unexpected certbot version found @@ -124,22 +119,3 @@ if ! diff letsencrypt-auto letsencrypt-auto-source/letsencrypt-auto ; then echo letsencrypt-auto and letsencrypt-auto-source/letsencrypt-auto differ exit 1 fi - -if [ "$RUN_RHEL6_TESTS" = 1 ]; then - # Add the SCL python release to PATH in order to resolve python3 command - PATH="/opt/rh/rh-python36/root/usr/bin:$PATH" - if ! command -v python3; then - echo "Python3 wasn't properly installed" - exit 1 - fi - if [ "$(/opt/eff.org/certbot/venv/bin/python -V 2>&1 | cut -d" " -f 2 | cut -d. -f1)" != 3 ]; then - echo "Python3 wasn't used in venv!" - exit 1 - fi - - if [ "$("$PYTHON_NAME" tools/readlink.py $OLD_VENV_PATH)" != "/opt/eff.org/certbot/venv" ]; then - echo symlink from old venv path not properly created! - exit 1 - fi -fi -echo upgrade appeared to be successful diff --git a/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh b/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh index 15cf9ee1b1c..fc54359166b 100755 --- a/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh +++ b/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh @@ -16,58 +16,14 @@ sudo chown root "$LE_AUTO_PATH" sudo chmod 0755 "$LE_AUTO_PATH" export PATH="$LE_AUTO_DIR:$PATH" -# On systems like Debian where certbot-auto is deprecated, we expect -# certbot-auto to error and refuse to install Certbot. Once certbot-auto is -# deprecated on RHEL systems, we can unconditionally run this code. -if [ -f /etc/debian_version ]; then - set +o pipefail - if ! letsencrypt-auto --debug --version | grep "Certbot cannot be installed."; then - echo "letsencrypt-auto didn't report being uninstallable." - exit 1 - fi - if [ ${PIPESTATUS[0]} != 1 ]; then - echo "letsencrypt-auto didn't exit with status 1 as expected" - exit 1 - fi - # letsencrypt-auto is deprecated and cannot be installed on this system so - # we cannot run the rest of this test. - exit 0 -fi - -letsencrypt-auto --os-packages-only --debug --version - -# This script sets the environment variables PYTHON_NAME, VENV_PATH, and -# VENV_SCRIPT based on the version of Python available on the system. For -# instance, Fedora uses Python 3 and Python 2 is not installed. -. tests/letstest/scripts/set_python_envvars.sh - -# Create a venv-like layout at the old virtual environment path to test that a -# symlink is properly created when letsencrypt-auto runs. -HOME=${HOME:-~root} -XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} -OLD_VENV_BIN="$XDG_DATA_HOME/letsencrypt/bin" -mkdir -p "$OLD_VENV_BIN" -touch "$OLD_VENV_BIN/letsencrypt" - -letsencrypt-auto certonly --no-self-upgrade -v --standalone --debug \ - --text --agree-tos \ - --renew-by-default --redirect \ - --register-unsafely-without-email \ - --domain $PUBLIC_HOSTNAME --server $BOULDER_URL - -LINK_PATH=$("$PYTHON_NAME" tools/readlink.py ${XDG_DATA_HOME:-~/.local/share}/letsencrypt) -if [ "$LINK_PATH" != "/opt/eff.org/certbot/venv" ]; then - echo symlink from old venv path not properly created! +# Since certbot-auto is deprecated, we expect certbot-auto to error and +# refuse to install Certbot. +set +o pipefail +if ! letsencrypt-auto --debug --version | grep "Certbot cannot be installed."; then + echo "letsencrypt-auto didn't report being uninstallable." exit 1 fi - -if ! letsencrypt-auto --help --no-self-upgrade | grep -F "letsencrypt-auto [SUBCOMMAND]"; then - echo "letsencrypt-auto not included in help output!" - exit 1 -fi - -OUTPUT_LEN=$(letsencrypt-auto --install-only --no-self-upgrade --quiet 2>&1 | wc -c) -if [ "$OUTPUT_LEN" != 0 ]; then - echo letsencrypt-auto produced unexpected output! +if [ ${PIPESTATUS[0]} != 1 ]; then + echo "letsencrypt-auto didn't exit with status 1 as expected" exit 1 fi diff --git a/tests/letstest/scripts/test_sdists.sh b/tests/letstest/scripts/test_sdists.sh index e3d9a8b8091..a038caff65f 100755 --- a/tests/letstest/scripts/test_sdists.sh +++ b/tests/letstest/scripts/test_sdists.sh @@ -8,12 +8,6 @@ VENV_PATH=venv3 # install OS packages sudo $BOOTSTRAP_SCRIPT -if command -v python && [ $(python -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//') -eq 26 ]; then - # RHEL/CentOS 6 will need a special treatment, so we need to detect that environment - # Enable the SCL Python 3.6 installed by letsencrypt-auto bootstrap - PATH="/opt/rh/rh-python36/root/usr/bin:$PATH" -fi - # setup venv # We strip the hashes because the venv creation script includes unhashed # constraints in the commands given to pip and the mix of hashed and unhashed diff --git a/tests/letstest/scripts/test_tests.sh b/tests/letstest/scripts/test_tests.sh index f62584709e2..f07e3b78e30 100755 --- a/tests/letstest/scripts/test_tests.sh +++ b/tests/letstest/scripts/test_tests.sh @@ -15,12 +15,6 @@ VENV_SCRIPT="tools/venv3.py" sudo $BOOTSTRAP_SCRIPT -if command -v python && [ $(python -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//') -eq 26 ]; then - # RHEL/CentOS 6 will need a special treatment, so we need to detect that environment - # Enable the SCL Python 3.6 installed by letsencrypt-auto bootstrap - PATH="/opt/rh/rh-python36/root/usr/bin:$PATH" -fi - cd $REPO_ROOT $VENV_SCRIPT . $VENV_NAME/bin/activate diff --git a/tests/letstest/targets.yaml b/tests/letstest/targets.yaml index 29edd1552c2..97c775f6c5f 100644 --- a/tests/letstest/targets.yaml +++ b/tests/letstest/targets.yaml @@ -52,17 +52,6 @@ targets: type: centos virt: hvm user: centos - # centos6 requires EPEL repo added - - ami: ami-1585c46a - name: centos6 - type: centos - virt: hvm - user: centos - userdata: | - #cloud-config - runcmd: - - yum install -y epel-release - - iptables -F - ami: ami-01ca03df4a6012157 name: centos8 type: centos diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt index 1d800172711..967596dedae 100644 --- a/tools/dev_constraints.txt +++ b/tools/dev_constraints.txt @@ -26,9 +26,15 @@ coverage==4.5.4 decorator==4.4.1 deprecated==1.2.10 dns-lexicon==3.3.17 -dnspython==1.15.0 -docker==3.7.2 -docker-compose==1.25.0 +# There is no version of dnspython that works on both Python 2 and Python 3.9. +# To work around this, we make use of the fact that subject to other +# constraints, pip will install the newest version of a package while ignoring +# versions that don't support the version of Python being used. The result of +# this is dnspython 2.0.0 is installed in Python 3 while dnspython 1.16.0 is +# installed in Python 2. +dnspython<=2.0.0 +docker==4.3.1 +docker-compose==1.26.2 docker-pycreds==0.4.0 dockerpty==0.4.1 docopt==0.6.2 @@ -94,8 +100,9 @@ pytest-sugar==0.9.2 pytest-rerunfailures==4.2 python-dateutil==2.8.1 python-digitalocean==1.11 +python-dotenv==0.14.0 pywin32==227 -PyYAML==3.13 +PyYAML==5.3.1 repoze.sphinx.autointerface==0.8 requests-file==1.4.2 requests-oauthlib==1.3.0 @@ -116,7 +123,7 @@ tox==3.14.0 tqdm==4.19.4 traitlets==4.3.3 twine==1.11.0 -typed-ast==1.4.0 +typed-ast==1.4.1 typing==3.6.4 uritemplate==3.0.0 virtualenv==16.6.2 diff --git a/tools/docker/core/Dockerfile b/tools/docker/core/Dockerfile index 02222008b88..0d3626853b3 100644 --- a/tools/docker/core/Dockerfile +++ b/tools/docker/core/Dockerfile @@ -42,9 +42,7 @@ RUN apk add --no-cache --virtual .build-deps \ musl-dev \ libffi-dev \ && python tools/pipstrap.py \ - && pip install --no-build-isolation \ - -r letsencrypt-auto-source/pieces/dependency-requirements.txt \ - && pip install --no-build-isolation --no-cache-dir --no-deps \ - --editable src/acme \ - --editable src/certbot \ -&& apk del .build-deps + && python tools/pip_install.py --no-cache-dir \ + --editable src/acme \ + --editable src/certbot \ + && apk del .build-deps diff --git a/tools/docker/plugin/Dockerfile b/tools/docker/plugin/Dockerfile index 5a6673e5b6a..863efd105c9 100644 --- a/tools/docker/plugin/Dockerfile +++ b/tools/docker/plugin/Dockerfile @@ -11,4 +11,4 @@ COPY qemu-${QEMU_ARCH}-static /usr/bin/ COPY . /opt/certbot/src/plugin # Install the DNS plugin -RUN tools/pip_install.py --no-cache-dir --editable /opt/certbot/src/plugin +RUN python tools/pip_install.py --no-cache-dir --editable /opt/certbot/src/plugin diff --git a/tools/finish_release.py b/tools/finish_release.py index bc8e832dfb7..24e20987fa9 100755 --- a/tools/finish_release.py +++ b/tools/finish_release.py @@ -21,6 +21,7 @@ python tools/finish_release.py ~/.ssh/githubpat.txt """ +import argparse import glob import os.path import re @@ -44,6 +45,34 @@ # for sanity checking. SNAP_ARCH_COUNT = 3 + +def parse_args(args): + """Parse command line arguments. + + :param args: command line arguments with the program name removed. This is + usually taken from sys.argv[1:]. + :type args: `list` of `str` + + :returns: parsed arguments + :rtype: argparse.Namespace + + """ + # Use the file's docstring for the help text and don't let argparse reformat it. + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('githubpat', help='path to your GitHub personal access token') + group = parser.add_mutually_exclusive_group() + # We use 'store_false' and a destination related to the other type of + # artifact to cause the flag being set to disable publishing of the other + # artifact. This makes using the parsed arguments later on a little simpler + # and cleaner. + group.add_argument('--snaps-only', action='store_false', dest='publish_windows', + help='Skip publishing other artifacts and only publish the snaps') + group.add_argument('--windows-only', action='store_false', dest='publish_snaps', + help='Skip publishing other artifacts and only publish the Windows installer') + return parser.parse_args(args) + + def download_azure_artifacts(tempdir): """Download and unzip build artifacts from Azure pipelines. @@ -181,8 +210,9 @@ def promote_snaps(version): def main(args): - github_access_token_file = args[0] + parsed_args = parse_args(args) + github_access_token_file = parsed_args.githubpat github_access_token = open(github_access_token_file, 'r').read().rstrip() with tempfile.TemporaryDirectory() as tempdir: @@ -191,8 +221,10 @@ def main(args): # again fails. Publishing the snaps can be done multiple times though # so we do that first to make it easier to run the script again later # if something goes wrong. - promote_snaps(version) - create_github_release(github_access_token, tempdir, version) + if parsed_args.publish_snaps: + promote_snaps(version) + if parsed_args.publish_windows: + create_github_release(github_access_token, tempdir, version) if __name__ == "__main__": main(sys.argv[1:]) diff --git a/tools/merge_requirements.py b/tools/merge_requirements.py index 0d41d12c464..bbcb38051fa 100755 --- a/tools/merge_requirements.py +++ b/tools/merge_requirements.py @@ -1,8 +1,9 @@ #!/usr/bin/env python """Merges multiple Python requirements files into one file. -Requirements files specified later take precedence over earlier ones. Only -simple SomeProject==1.2.3 format is currently supported. +Requirements files specified later take precedence over earlier ones. +Only the simple formats SomeProject==1.2.3 or SomeProject<=1.2.3 are +currently supported. """ from __future__ import print_function @@ -16,17 +17,28 @@ def process_entries(entries): :param list entries: List of entries - :returns: mapping from a project to its pinned version + :returns: mapping from a project to its version specifier :rtype: dict """ data = {} for e in entries: e = e.strip() if e and not e.startswith('#') and not e.startswith('-e'): - project, version = e.split('==') - if not version: + # Support for <= was added as part of + # https://github.com/certbot/certbot/pull/8460 because we weren't + # able to pin a package to an exact version. Normally, this + # functionality shouldn't be needed so we could remove it in the + # future. If you do so, make sure to update other places in this + # file related to this behavior such as this file's docstring. + for comparison in ('==', '<=',): + parts = e.split(comparison) + if len(parts) == 2: + project_name = parts[0] + version = parts[1] + data[project_name] = comparison + version + break + else: raise ValueError("Unexpected syntax '{0}'".format(e)) - data[project] = version return data def read_file(file_path): @@ -44,10 +56,11 @@ def read_file(file_path): def output_requirements(requirements): """Prepare print requirements to stdout. - :param dict requirements: mapping from a project to its pinned version + :param dict requirements: mapping from a project to its version + specifier """ - return '\n'.join('{0}=={1}'.format(key, value) + return '\n'.join('{0}{1}'.format(key, value) for key, value in sorted(requirements.items())) diff --git a/tools/pip_install.py b/tools/pip_install.py index f963e466010..c1c81482bc2 100755 --- a/tools/pip_install.py +++ b/tools/pip_install.py @@ -59,9 +59,13 @@ def certbot_normal_processing(tools_path, test_constraints): certbot_requirements = os.path.normpath(os.path.join( repo_path, 'letsencrypt-auto-source/pieces/dependency-requirements.txt')) with open(certbot_requirements, 'r') as fd: - data = fd.readlines() + certbot_reqs = fd.readlines() + with open(os.path.join(tools_path, 'pipstrap_constraints.txt'), 'r') as fd: + pipstrap_reqs = fd.readlines() with open(test_constraints, 'w') as fd: - data = "\n".join(strip_hashes.process_entries(data)) + data_certbot = "\n".join(strip_hashes.process_entries(certbot_reqs)) + data_pipstrap = "\n".join(strip_hashes.process_entries(pipstrap_reqs)) + data = "\n".join([data_certbot, data_pipstrap]) fd.write(data) @@ -72,7 +76,8 @@ def merge_requirements(tools_path, requirements, test_constraints, all_constrain # Here is the order by increasing priority: # 1) The general development constraints (tools/dev_constraints.txt) # 2) The general tests constraints (oldest_requirements.txt or - # certbot-auto's dependency-requirements.txt for the normal processing) + # certbot-auto's dependency-requirements.txt + pipstrap's constraints + # for the normal processing) # 3) The local requirement file, typically local-oldest-requirement in oldest tests files = [os.path.join(tools_path, 'dev_constraints.txt'), test_constraints] if requirements: @@ -82,17 +87,18 @@ def merge_requirements(tools_path, requirements, test_constraints, all_constrain fd.write(merged_requirements) -def call_with_print(command): +def call_with_print(command, env=None): + if not env: + env = os.environ print(command) - subprocess.check_call(command, shell=True) + subprocess.check_call(command, shell=True, env=env) -def pip_install_with_print(args_str, disable_build_isolation=True): - command = ['"', sys.executable, '" -m pip install --disable-pip-version-check '] - if disable_build_isolation: - command.append('--no-build-isolation ') - command.append(args_str) - call_with_print(''.join(command)) +def pip_install_with_print(args_str, env=None): + if not env: + env = os.environ + command = ['"', sys.executable, '" -m pip install --disable-pip-version-check ', args_str] + call_with_print(''.join(command), env=env) def main(args): @@ -113,20 +119,22 @@ def main(args): else: certbot_normal_processing(tools_path, test_constraints) + env = os.environ.copy() + env["PIP_CONSTRAINT"] = all_constraints + merge_requirements(tools_path, requirements, test_constraints, all_constraints) if requirements: # This branch is executed during the oldest tests # First step, install the transitive dependencies of oldest requirements # in respect with oldest constraints. - pip_install_with_print('--constraint "{0}" --requirement "{1}"' - .format(all_constraints, requirements)) + pip_install_with_print('--requirement "{0}"'.format(requirements), + env=env) # Second step, ensure that oldest requirements themselves are effectively # installed using --force-reinstall, and avoid corner cases like the one described # in https://github.com/certbot/certbot/issues/7014. pip_install_with_print('--force-reinstall --no-deps --requirement "{0}"' .format(requirements)) - pip_install_with_print('--constraint "{0}" {1}'.format( - all_constraints, ' '.join(args))) + pip_install_with_print(' '.join(args), env=env) if __name__ == '__main__': diff --git a/tools/pipstrap.py b/tools/pipstrap.py index 2f21a9a5fdd..e6b74691658 100755 --- a/tools/pipstrap.py +++ b/tools/pipstrap.py @@ -1,46 +1,17 @@ #!/usr/bin/env python """Uses pip to upgrade Python packaging tools to pinned versions.""" from __future__ import absolute_import - import os -import shutil -import tempfile import pip_install -# We include the hashes of the packages here for extra verification of -# the packages downloaded from PyPI. This is especially valuable in our -# builds of Certbot that we ship to our users such as our Docker images. -# -# An older version of setuptools is currently used here in order to keep -# compatibility with Python 2 since newer versions of setuptools have dropped -# support for it. -REQUIREMENTS = r""" -pip==20.2.4 \ - --hash=sha256:51f1c7514530bd5c145d8f13ed936ad6b8bfcb8cf74e10403d0890bc986f0033 \ - --hash=sha256:85c99a857ea0fb0aedf23833d9be5c40cf253fe24443f0829c7b472e23c364a1 -setuptools==44.1.1 \ - --hash=sha256:27a714c09253134e60a6fa68130f78c7037e5562c4f21f8f318f2ae900d152d5 \ - --hash=sha256:c67aa55db532a0dadc4d2e20ba9961cbd3ccc84d544e9029699822542b5a476b -wheel==0.35.1 \ - --hash=sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2 \ - --hash=sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f -""" +_REQUIREMENTS_PATH = os.path.join(os.path.dirname(__file__), "pipstrap_constraints.txt") def main(): - with pip_install.temporary_directory() as tempdir: - requirements_filepath = os.path.join(tempdir, 'reqs.txt') - with open(requirements_filepath, 'w') as f: - f.write(REQUIREMENTS) - pip_install_args = '--requirement ' + requirements_filepath - # We don't disable build isolation because we may have an older - # version of pip that doesn't support the flag disabling it. We - # expect these packages to already have usable wheels available - # anyway so no building should be required. - pip_install.pip_install_with_print(pip_install_args, - disable_build_isolation=False) + pip_install_args = '--requirement "{0}"'.format(_REQUIREMENTS_PATH) + pip_install.pip_install_with_print(pip_install_args) if __name__ == '__main__': diff --git a/tools/pipstrap_constraints.txt b/tools/pipstrap_constraints.txt new file mode 100644 index 00000000000..5de9e147df5 --- /dev/null +++ b/tools/pipstrap_constraints.txt @@ -0,0 +1,18 @@ +# Constraints for pipstrap.py +# +# We include the hashes of the packages here for extra verification of +# the packages downloaded from PyPI. This is especially valuable in our +# builds of Certbot that we ship to our users such as our Docker images. +# +# An older version of setuptools is currently used here in order to keep +# compatibility with Python 2 since newer versions of setuptools have dropped +# support for it. +pip==20.2.4 \ + --hash=sha256:51f1c7514530bd5c145d8f13ed936ad6b8bfcb8cf74e10403d0890bc986f0033 \ + --hash=sha256:85c99a857ea0fb0aedf23833d9be5c40cf253fe24443f0829c7b472e23c364a1 +setuptools==44.1.1 \ + --hash=sha256:27a714c09253134e60a6fa68130f78c7037e5562c4f21f8f318f2ae900d152d5 \ + --hash=sha256:c67aa55db532a0dadc4d2e20ba9961cbd3ccc84d544e9029699822542b5a476b +wheel==0.35.1 \ + --hash=sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2 \ + --hash=sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f diff --git a/tools/snap/build_remote.py b/tools/snap/build_remote.py index 285521190b6..e6a44240f81 100755 --- a/tools/snap/build_remote.py +++ b/tools/snap/build_remote.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 import argparse -import glob import datetime -from multiprocessing import Pool, Process, Manager, Event +import glob import re import subprocess import sys -import tempfile +import time +from multiprocessing import Pool, Process, Manager from os.path import join, realpath, dirname, basename, exists - CERTBOT_DIR = dirname(dirname(dirname(realpath(__file__)))) PLUGINS = [basename(path) for path in glob.glob(join(CERTBOT_DIR, 'certbot-dns-*'))] def _execute_build(target, archs, status, workspace): process = subprocess.Popen([ - 'snapcraft', 'remote-build', '--launchpad-accept-public-upload', '--recover', '--build-on', ','.join(archs) + 'snapcraft', 'remote-build', '--launchpad-accept-public-upload', '--recover', + '--build-on', ','.join(archs) ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, cwd=workspace) process_output = [] @@ -27,7 +27,7 @@ def _execute_build(target, archs, status, workspace): return process.wait(), process_output -def _build_snap(target, archs, status, lock): +def _build_snap(target, archs, status, running, lock): status[target] = {arch: '...' for arch in archs} if target == 'certbot': @@ -39,7 +39,8 @@ def _build_snap(target, archs, status, lock): while retry: exit_code, process_output = _execute_build(target, archs, status, workspace) - print(f'Build {target} for {",".join(archs)} (attempt {4-retry}/3) ended with exit code {exit_code}.') + print(f'Build {target} for {",".join(archs)} (attempt {4-retry}/3) ended with ' + f'exit code {exit_code}.') sys.stdout.flush() with lock: @@ -49,7 +50,8 @@ def _build_snap(target, archs, status, lock): # We expect to have all target snaps available, or something bad happened. snaps_list = glob.glob(join(workspace, '*.snap')) if not len(snaps_list) == len(archs): - print(f'Some of the expected snaps for a successful build are missing (current list: {snaps_list}).') + print('Some of the expected snaps for a successful build are missing ' + f'(current list: {snaps_list}).') dump_output = True else: break @@ -63,9 +65,12 @@ def _build_snap(target, archs, status, lock): print(f'Dumping snapcraft remote-build output build for {target}:') print('\n'.join(process_output)) - # Retry the remote build if it has been interrupted (non zero status code) or if some builds have failed. + # Retry the remote build if it has been interrupted (non zero status code) + # or if some builds have failed. retry = retry - 1 + running[target] = False + return {target: workspace} @@ -96,15 +101,11 @@ def _dump_status_helper(archs, status): sys.stdout.flush() -def _dump_status(archs, status, stop_event): - while not stop_event.wait(10): - print('Remote build status at {0}'.format(datetime.datetime.now())) +def _dump_status(archs, status, running): + while any(running.values()): + print(f'Remote build status at {datetime.datetime.now()}') _dump_status_helper(archs, status) - - -def _dump_status_final(archs, status): - print('Results for remote build finished at {0}'.format(datetime.datetime.now())) - _dump_status_helper(archs, status) + time.sleep(10) def _dump_results(targets, archs, status, workspaces): @@ -120,10 +121,10 @@ def _dump_results(targets, archs, status, workspaces): if not exists(build_output_path): build_output = f'No output has been dumped by snapcraft remote-build.' else: - with open(join(workspaces[target], '{0}_{1}.txt'.format(target, arch))) as file_h: + with open(join(workspaces[target], f'{target}_{arch}.txt')) as file_h: build_output = file_h.read() - print('Output for failed build target={0} arch={1}'.format(target, arch)) + print(f'Output for failed build target={target} arch={arch}') print('-------------------------------------------') print(build_output) print('-------------------------------------------') @@ -134,6 +135,10 @@ def _dump_results(targets, archs, status, workspaces): else: print('Some builds failed.') + print() + print(f'Results for remote build finished at {datetime.datetime.now()}') + _dump_status_helper(archs, status) + return failures @@ -143,6 +148,8 @@ def main(): help='the list of snaps to build') parser.add_argument('--archs', nargs='+', choices=['amd64', 'arm64', 'armhf'], default=['amd64'], help='the architectures for which snaps are built') + parser.add_argument('--timeout', type=int, default=None, + help='build process will fail after the provided timeout (in seconds)') args = parser.parse_args() archs = set(args.archs) @@ -158,7 +165,7 @@ def main(): # If we're building anything other than just Certbot, we need to # generate the snapcraft files for the DNS plugins. - if targets != set(('certbot',)): + if targets != {'certbot'}: subprocess.run(['tools/snap/generate_dnsplugins_all.sh'], check=True, cwd=CERTBOT_DIR) @@ -169,25 +176,29 @@ def main(): with Manager() as manager, Pool(processes=len(targets)) as pool: status = manager.dict() + running = manager.dict({target: True for target in targets}) lock = manager.Lock() - stop_event = Event() - state_process = Process(target=_dump_status, args=(archs, status, stop_event)) - state_process.start() + async_results = [pool.apply_async(_build_snap, (target, archs, status, running, lock)) + for target in targets] - async_results = [pool.apply_async(_build_snap, (target, archs, status, lock)) for target in targets] + process = Process(target=_dump_status, args=(archs, status, running)) + process.start() - workspaces = {} - for async_result in async_results: - workspaces.update(async_result.get()) + try: + process.join(args.timeout) - stop_event.set() - state_process.join() + if process.is_alive(): + raise ValueError(f"Timeout out reached ({args.timeout} seconds) during the build!") - failures = _dump_results(targets, archs, status, workspaces) - _dump_status_final(archs, status) + workspaces = {} + for async_result in async_results: + workspaces.update(async_result.get()) - return 1 if failures else 0 + if _dump_results(targets, archs, status, workspaces): + raise ValueError("There were failures during the build!") + finally: + process.terminate() if __name__ == '__main__': diff --git a/tox.ini b/tox.ini index 5dcc55d3fa2..212d4ee76c6 100644 --- a/tox.ini +++ b/tox.ini @@ -40,6 +40,7 @@ install_packages = source_paths = acme/acme certbot/certbot + certbot-ci/certbot_integration_tests certbot-apache/certbot_apache certbot-compatibility-test/certbot_compatibility_test certbot-dns-cloudflare/certbot_dns_cloudflare @@ -188,29 +189,6 @@ whitelist_externals = passenv = DOCKER_* -[testenv:le_auto_centos6] -# At the moment, this tests under Python 2.6 only, as only that version is -# readily available on the CentOS 6 Docker image. -commands = - python {toxinidir}/tests/modification-check.py - docker build -f letsencrypt-auto-source/Dockerfile.redhat6 --build-arg REDHAT_DIST_FLAVOR=centos -t lea letsencrypt-auto-source - docker run --rm -t lea -whitelist_externals = - docker -passenv = - DOCKER_* - TARGET_BRANCH - -[testenv:le_auto_oraclelinux6] -# At the moment, this tests under Python 2.6 only, as only that version is -# readily available on the Oracle Linux 6 Docker image. -commands = - docker build -f letsencrypt-auto-source/Dockerfile.redhat6 --build-arg REDHAT_DIST_FLAVOR=oraclelinux -t lea letsencrypt-auto-source - docker run --rm -t lea -whitelist_externals = - docker -passenv = DOCKER_* - [testenv:docker_dev] # tests the Dockerfile-dev file to ensure development with it works # as expected @@ -240,6 +218,17 @@ commands = --cov-config=certbot-ci/certbot_integration_tests/.coveragerc coverage report --include 'certbot/*' --show-missing --fail-under=62 +[testenv:integration-dns-rfc2136] +commands = + {[base]pip_install} acme certbot certbot-dns-rfc2136 certbot-ci + pytest certbot-ci/certbot_integration_tests/rfc2136_tests \ + --acme-server=pebble --dns-server=bind \ + --numprocesses=1 \ + --cov=acme --cov=certbot --cov=certbot_dns_rfc2136 --cov-report= \ + --cov-config=certbot-ci/certbot_integration_tests/.coveragerc + coverage report --include 'certbot/*' --show-missing --fail-under=45 + coverage report --include 'certbot-dns-rfc2136/*' --show-missing --fail-under=87 + [testenv:integration-external] # Run integration tests with Certbot outside of tox's virtual environment. commands = diff --git a/windows-installer/construct.py b/windows-installer/construct.py index 14f77095900..1ce4811acd3 100644 --- a/windows-installer/construct.py +++ b/windows-installer/construct.py @@ -46,9 +46,11 @@ def _compile_wheels(repo_path, build_path, venv_python): wheels_project = [os.path.join(repo_path, package) for package in certbot_packages] with _prepare_constraints(repo_path) as constraints_file_path: - command = [venv_python, '-m', 'pip', 'wheel', '-w', wheels_path, '--constraint', constraints_file_path] + env = os.environ.copy() + env['PIP_CONSTRAINT'] = constraints_file_path + command = [venv_python, '-m', 'pip', 'wheel', '-w', wheels_path] command.extend(wheels_project) - subprocess.check_call(command) + subprocess.check_call(command, env=env) def _prepare_build_tools(venv_path, venv_python, repo_path): @@ -61,15 +63,20 @@ def _prepare_build_tools(venv_path, venv_python, repo_path): @contextlib.contextmanager def _prepare_constraints(repo_path): - requirements = os.path.join(repo_path, 'letsencrypt-auto-source', 'pieces', 'dependency-requirements.txt') - constraints = subprocess.check_output( - [sys.executable, os.path.join(repo_path, 'tools', 'strip_hashes.py'), requirements], + reqs_certbot = os.path.join(repo_path, 'letsencrypt-auto-source', 'pieces', 'dependency-requirements.txt') + reqs_pipstrap = os.path.join(repo_path, 'tools', 'pipstrap_constraints.txt') + constraints_certbot = subprocess.check_output( + [sys.executable, os.path.join(repo_path, 'tools', 'strip_hashes.py'), reqs_certbot], + universal_newlines=True) + constraints_pipstrap = subprocess.check_output( + [sys.executable, os.path.join(repo_path, 'tools', 'strip_hashes.py'), reqs_pipstrap], universal_newlines=True) workdir = tempfile.mkdtemp() try: constraints_file_path = os.path.join(workdir, 'constraints.txt') with open(constraints_file_path, 'a') as file_h: - file_h.write(constraints) + file_h.write(constraints_pipstrap) + file_h.write(constraints_certbot) file_h.write('pywin32=={0}'.format(PYWIN32_VERSION)) yield constraints_file_path finally: