Development

This chapter details how the package’s tests are run and gives an overview of all base classes and public interfaces.

Tests

The tests are run using the unittest framework. Each test has a corresponding section in the debug.yml that configures the tested apps. The section’s name is defined by the property config_sec that each test overrides.

To run any of these, the acmetk package first needs to be installed:

git clone https://github.com/noahkw/acmetk.git
cd acmetk/
pip install .

Furthermore, a PostgreSQL instance needs to be running locally. This is easily achieved using docker-compose:

Put the following docker-compose.yml in ~/acme-pg or any other empty folder:

version: "2.1"
services:
  db:
    image: postgres
    volumes:
      - ./db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${ACME_TESTS_PG_PW}
      POSTGRES_USER: acme-broker
    ports:
      - 55432:5432

Create a .env file alongside it:

ACME_TESTS_PG_PW=acme-broker-debug-pw

Create the acme-ca table:

# Start the container
sudo docker-compose up -d
# Find out the container name
sudo docker ps # The correct name should end in _db_1
# Substitute NAME with the container's name
sudo docker exec -it NAME createdb -U acme-broker acme-ca

Test clients

The server implementations are all tested against the following clients:

There is one test_run() test function per client that tests the general certificate acquisition process from creating an account to downloading the certificate.

Furthermore, the Certbot subclasses have the following test functions:

  • test_subdomain_revocation(): Acquires a certificate for the configured domain itself and for the subdomains dns.domain as well as http.domain setting the preferred challenge to DNS-01 and HTTP-01 respectively. The three resulting certificates are then revoked.

  • test_skey_revocation(): Acquires a certificate for the configured domain and then revokes it, signing the request using the certificate’s private key.

  • test_renewal(): Acquires a certificate for the configured domain and then renews it.

  • test_register(): Tests the account creation process.

  • test_unregister(): Registers an account, then deactivates that account.

The OurClient (AcmeClient) subclasses have the following additional test functions:

  • test_run_stress(): Carries out ten general certificate acquisition processes in parallel.

  • test_revoke(): Acquires a certificate for the configured domain and then revokes it, signing the request using the account’s private key.

  • test_account_update(): Registers an account, then updates the associated contact information.

  • test_email_validation(): Registers an account, then updates the associated contact information specifying an email address with a suffix that is not whitelisted.

  • test_unregister(): Registers an account, then deactivates that account.

AcmeCA Tests

Tests the integration of the AcmeCA against various test clients.

To run all of the tests:

mkdir /tmp/dehydrated
curl https://raw.githubusercontent.com/dehydrated-io/dehydrated/master/dehydrated -o /tmp/dehydrated/dehydrated
chmod +x /tmp/dehydrated/dehydrated
cd tests
python -m unittest test_ca.py

AcmeBroker/AcmeProxy Tests

Tests the integration of the AcmeBroker/AcmeProxy against two certificate authorities, namely a local AcmeCA instance and Let’s Encrypt staging. The integration with the various clients is tested at the same time. There is one caveat: The TestBrokerLE/TestProxyLE subclasses, meaning those that test against LE staging, need to be run from a machine that has write access to, in our case, the Infoblox instance. This may differ depending on which challenge solver plugin is used.

To run all of the tests:

cd tests
# AcmeBroker
python -m unittest test_broker.py
# AcmeProxy
python -m unittest test_proxy.py

InfobloxClient Tests

Tests the main functionality of the InfobloxClient to set and delete DNS TXT records. The credentials except for the password need to be stored inside the infoblox section of the debug.yml. The DNS servers and default views should also be changed to be compatible with the individual infrastructure.

To run all of the tests:

cd tests
echo "YOUR_PASSWORD" > ../infoblox
# Create the file that contains the Infoblox password
python -m unittest test_infoblox.py

Deployment Tests

Tests the acmetk.server.AcmeCA behind a reverse proxy inside a docker container spun up by docker-compose. To configure running these tests, PyCharm’s docker-compose remote python interpreter functionality may be leveraged. Steps to create a remote interpreter these tests can run in:

  • Click Python 3.x (Venv name) in the bottom right corner and select Add Interpreter…

  • Select docker-compose on the left side in the new window

  • Add docker-compose.dev.yml as a second configuration file

  • Select app as the service

  • Click OK

To run all of the tests:

First, create a new .env file with the following contents in the repository’s root folder.

ACME_SUPERUSER_PW=acmesupw

Select the new docker-compose interpreter from the bottom right, right click test_deployment.py in the project view and select “Run ‘Unittests’ in test_deployment.py”.

Contributing

Set up your development environment:

# Clone the repo
git clone |GIT_URL|
cd acmetk/
# Create a virtual environment
python -m venv venv
source venv/bin/activate
# Install the requirements
pip install -r requirements.txt
pip install -r requirements-dev.txt
# Install the package in dev mode
pip install -e .
# Install the pre-commit hook for linting, formatting, etc.
pre-commit install

Abstract Base Classes

AcmeServerBase

AcmeServerBase is the base class for all ACME-compliant server implementations. It encapsulates a aiohttp.web.Application to respond to ACME requests and aiohttp_jinja2 is used as the template engine to render the AcmeManagement and AcmeEAB sites.

Subclasses need to implement the methods certificate() and handle_order_finalize(). Subclasses must also set the config_name which corresponds to the section name in the config files. Instances should only be created using create_app() which instantiates the server and attaches the database session at the least.

To run a new server from the CLI, a run_servername() function, which is called if app_class is the server class, should be created in acmetk.main.py. Any challenge validators, internal clients, etc., as well as the server itself, should be instantiated there. The runner is then returned along with the server instance.

AcmeRelayBase

AcmeRelayBase inherits from AcmeServerBase. It features an internal AcmeClient that is used to communicate with another certificate authority of choice. Subclasses need to implement the method handle_order_finalize().

If complex configuration beyond the server itself and its internal client is not needed, then the existing run_relay() in acmetk.main.py may be used to start the server.

Challenge Solver

ChallengeSolver is the interface that challenge solver plugins must implement. Implementations must also be registered with the plugin registry via acmetk.server.PluginRegistry.register_plugin(), so that the CLI script knows which configuration option corresponds to which challenge solver class. A template for a challenge solver plugin can be found in acmetk/plugins/template_solver.py.

connect() may be overridden if the plugin needs to connect to some resource before being able to challenge completion requests.

complete_challenge() must be overridden by all plugin implementations. It is passed the account key, as well as the challenge and the identifier associated with the challenge. Upon being called, the method needs to complete the challenge, i.e. by provisioning some resource, and then defer returning until the remote CA is allowed to validate the challenge.

complete_challenge() must also be overridden by all plugin implementations. Upon being called, it should de-provision the resources that were provisioned by the solver to complete that specific challenge.

Configuration options inside the challenge_solver section of the client’s block are directly passed to the constructor as keyword arguments. If our sublass were called xyzdns, for example, then host="example.xyz" would be passed in the following example:

client:
  challenge_solver:
    xyzdns:
      host: 'example.xyz'

Challenge Validator

ChallengeValidator is the interface that challenge validator plugins must implement. Implementations must also be registered with the plugin registry via acmetk.server.PluginRegistry.register_plugin(), so that the CLI script knows which configuration option corresponds to which challenge validator class. A template for a challenge validator plugin can be found in acmetk/plugins/template_validator.py

validate_challenge() must be overridden by all plugin implementations. It is passed the challenge as well as any number of keyword arguments. Upon being called, the method should attempt to validate the challenge. If the validation was successful, then the method should just return. Otherwise, a CouldNotValidateChallenge exception must be raised.