From 813f8e92fe2b005c5d23f786c7f946d08bb92dda Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 4 Nov 2024 20:08:47 -0700 Subject: [PATCH 1/4] feat: add verify_ssl argument to disable cert verification to the acme server --- simple_acme_dns/__init__.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/simple_acme_dns/__init__.py b/simple_acme_dns/__init__.py index dfdddaa..c0f026f 100644 --- a/simple_acme_dns/__init__.py +++ b/simple_acme_dns/__init__.py @@ -35,6 +35,7 @@ from . import errors from . import tools + # Constants and Variables DNS_LABEL = '_acme-challenge' __pdoc__ = {"tests": False} # Excludes 'tests' submodule from documentation @@ -54,7 +55,8 @@ def __init__( directory: str = "https://acme-staging-v02.api.letsencrypt.org/directory", nameservers: list = None, new_account: bool = False, - generate_csr: bool = False + generate_csr: bool = False, + verify_ssl: bool = True ): """ Args: @@ -64,6 +66,8 @@ def __init__( nameservers (list): A list of DNS server hosts to query when checking DNS propagation. new_account (bool): Automatically create a new ACME account upon creation. generate_csr (bool): Automatically generate a new private key and CSR upon creation. + verify_ssl (bool): Verify the SSL certificate of the ACME server when making requests. This only applies + when creating a new account. Examples: >>> import simple_acme_dns @@ -97,7 +101,7 @@ def __init__( # Automatically create a new account if requested if new_account: - self.new_account() + self.new_account(verify_ssl=verify_ssl) # Automatically create a new private key and CSR if generate_csr: self.generate_private_key_and_csr() @@ -281,11 +285,14 @@ def revoke_certificate(self, reason: int = 0) -> None: cert_obj = jose.ComparableX509(OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, self.certificate)) self.acme_client.revoke(cert_obj, reason) - def new_account(self) -> None: + def new_account(self, verify_ssl=True) -> None: """ Registers a new ACME account at the set ACME `directory` URL. By running this method, you are agreeing to the ACME servers terms of use. + Args: + verify_ssl (bool): Verify the SSL certificate of the ACME server when making requests. + Examples: >>> client.new_account() """ @@ -294,7 +301,7 @@ def new_account(self) -> None: self.account_key = jose.JWKRSA(key=rsa_key) # Initialize our ACME client object - self.net = client.ClientNetwork(self.account_key, user_agent='simple_acme_dns/v2') + self.net = client.ClientNetwork(self.account_key, user_agent='simple_acme_dns/v2', verify_ssl=verify_ssl) self.directory_obj = messages.Directory.from_json(self.net.get(self.directory).json()) self.acme_client = client.ClientV2(self.directory_obj, net=self.net) @@ -346,6 +353,7 @@ def export_account(self, save_certificate: bool = True, save_private_key: bool = 'account': self.account.to_json(), 'account_key': self.account_key.json_dumps(), 'directory': self.directory, + 'verify_ssl': self.net.verify_ssl, 'domains': self._domains, 'certificate': self.certificate.decode() if save_certificate else '', 'private_key': self.private_key.decode() if save_private_key else '' @@ -412,6 +420,7 @@ def load_account(json_data: str) -> 'ACMEClient': obj = ACMEClient() # Format the serialized data back into the object + verify_ssl = acct_data.get('verify_ssl', True) obj.directory = acct_data.get('directory', None) obj.domains = acct_data.get('domains', []) obj.certificate = acct_data.get('certificate', '').encode() @@ -422,7 +431,7 @@ def load_account(json_data: str) -> 'ACMEClient': obj.account_key = jose.JWKRSA.json_loads(acct_data['account_key']) # Re-initialize the ACME client and registration - obj.net = client.ClientNetwork(obj.account_key, user_agent='simple_acme_dns/1.0.0') + obj.net = client.ClientNetwork(obj.account_key, user_agent='simple_acme_dns/1.0.0', verify_ssl=verify_ssl) obj.directory_obj = messages.Directory.from_json(obj.net.get(obj.directory).json()) obj.acme_client = client.ClientV2(obj.directory_obj, net=obj.net) obj.account = obj.acme_client.query_registration(obj.account) From 92fe808879cda12ce994559e83d73ef315c6a7f8 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 4 Nov 2024 20:09:14 -0700 Subject: [PATCH 2/4] tests: update tests to support using pebble --- simple_acme_dns/tests/test_simple_acme_dns.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/simple_acme_dns/tests/test_simple_acme_dns.py b/simple_acme_dns/tests/test_simple_acme_dns.py index 68493ca..333d6a0 100644 --- a/simple_acme_dns/tests/test_simple_acme_dns.py +++ b/simple_acme_dns/tests/test_simple_acme_dns.py @@ -14,6 +14,7 @@ """Tests primary functionality of the simple_acme_dns package.""" import os import random +import time import unittest import acme.messages @@ -25,7 +26,7 @@ BASE_DOMAIN = "testing.jaredhendrickson.com" TEST_DOMAINS = [f"{random.randint(10000, 99999)}.simple-acme-dns.{BASE_DOMAIN}"] TEST_EMAIL = f"simple-acme-dns@{BASE_DOMAIN}" -TEST_DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory" +TEST_DIRECTORY = os.environ.get("ACME_DIRECTORY", "https://acme-staging-v02.api.letsencrypt.org/directory") TEST_NAMESERVERS = ["8.8.8.8", "1.1.1.1"] unittest.TestLoader.sortTestMethodsUsing = None # Ensure tests run in order @@ -42,7 +43,8 @@ def setUpClass(cls): domains=TEST_DOMAINS, email=TEST_EMAIL, directory=TEST_DIRECTORY, - nameservers=TEST_NAMESERVERS + nameservers=TEST_NAMESERVERS, + verify_ssl=False ) @classmethod @@ -77,7 +79,7 @@ def test_generate_keys_and_csr(self): def test_account_enrollment(self): """Test to ensure ACME accounts are successfully registered.""" - self.client.new_account() + self.client.new_account(verify_ssl=False) self.assertIsNotNone(self.client.account) self.assertIsNotNone(self.client.account_key) self.assertTrue(is_json(self.client.export_account())) @@ -90,7 +92,8 @@ def test_shorthand_enrollment_and_csr(self): directory=TEST_DIRECTORY, nameservers=TEST_NAMESERVERS, new_account=True, - generate_csr=True + generate_csr=True, + verify_ssl=False ) # Ensure there is an account enrolled and a CSR/private key created @@ -111,7 +114,7 @@ def test_acme_verification_and_cert(self): """Test to ensure ACME verification works and a certificate is obtained.""" # Request verification tokens self.client.generate_private_key_and_csr() - self.client.new_account() + self.client.new_account(verify_ssl=False) self.assertIsInstance(self.client.request_verification_tokens(), dict) # Before we actually create the DNS entries, ensure DNS propagation checks fail as expected @@ -133,6 +136,7 @@ def test_acme_verification_and_cert(self): ) # Request the certificates and ensure a certificate is received + time.sleep(15) # Wait for DNS to propagate self.assertTrue(is_cert(self.client.request_certificate())) def test_account_exports(self): @@ -189,7 +193,8 @@ def test_delete_account_file(self): directory=TEST_DIRECTORY, nameservers=TEST_NAMESERVERS, new_account=True, - generate_csr=True + generate_csr=True, + verify_ssl=False ) client.account_path = "INVALID_FILE.json" self.assertIsNone(client.deactivate_account(delete=True)) @@ -205,7 +210,8 @@ def test_email_property_when_email_is_not_set(self): client = simple_acme_dns.ACMEClient( domains=TEST_DOMAINS, directory=TEST_DIRECTORY, - nameservers=TEST_NAMESERVERS + nameservers=TEST_NAMESERVERS, + verify_ssl=False ) return client.email From fe24cd1c2d49d25d138485b5e4717253dcf5b1bb Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 4 Nov 2024 20:09:39 -0700 Subject: [PATCH 3/4] chore: allow 8 args max when linting --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 4bed677..b12e4f4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -433,7 +433,7 @@ exclude-too-few-public-methods= ignored-parents= # Maximum number of arguments for function / method. -max-args=7 +max-args=8 # Maximum number of attributes for a class (see R0902). max-attributes=22 From 82e39a300ec9ce4431fc3c34611d0d052ad3c77e Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 4 Nov 2024 20:09:55 -0700 Subject: [PATCH 4/4] ci: use pebble for unit tests --- .github/workflows/quality.yml | 11 ++++++++++ simple_acme_dns/tests/docker-compose.yml | 27 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 simple_acme_dns/tests/docker-compose.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 84929ad..2ec00a8 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -48,7 +48,18 @@ jobs: python3 -m pip install . python3 -m pip install -r requirements-dev.txt + - name: Install Docker Compose + run: | + curl -L "https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r '.tag_name')/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + docker-compose --version + + - name: Start Pebble server + run: docker-compose -f simple_acme_dns/tests/docker-compose.yml up -d + - name: Test simple_acme_dns package to ensure 100% coverage + env: + ACME_DIRECTORY: 'https://localhost:14000/dir' run: | python3 -m coverage run --module unittest simple_acme_dns/tests/test_* python3 -m coverage report --show-missing --fail-under 100 diff --git a/simple_acme_dns/tests/docker-compose.yml b/simple_acme_dns/tests/docker-compose.yml new file mode 100644 index 0000000..7c2f55b --- /dev/null +++ b/simple_acme_dns/tests/docker-compose.yml @@ -0,0 +1,27 @@ +# Docker compose file for setting up a Pebble ACME and challenge server for testing +version: "3" +services: + pebble: + image: ghcr.io/letsencrypt/pebble:latest + command: -config test/config/pebble-config.json -strict -dnsserver 1.1.1.1:53 + ports: + - '14000:14000' # HTTPS ACME API + - '15000:15000' # HTTPS Management API + networks: + acmenet: + ipv4_address: 10.30.50.2 + challtestsrv: + image: ghcr.io/letsencrypt/pebble-challtestsrv:latest + command: -defaultIPv6 "" -defaultIPv4 10.30.50.3 + ports: + - 8055:8055 # HTTP Management API + networks: + acmenet: + ipv4_address: 10.30.50.3 +networks: + acmenet: + driver: bridge + ipam: + driver: default + config: + - subnet: 10.30.50.0/24 \ No newline at end of file