diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 5beec09..88a6fb6 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -8,14 +8,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version: '^1.13.1' - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Set up peeble @@ -33,4 +33,6 @@ jobs: export GOPATH=~/go ./scripts/pebble_service.sh start tests/conf/pebble-config.json behave tests/features - ./scripts/pebble_service.sh stop tests/conf/pebble-config.json + - name: Build packages + run: | + ./scripts/build.sh diff --git a/LICENSE b/LICENSE index b842d30..adce694 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2019-2022 Flávio Gonçalves Garcia + Copyright 2019-2025 Flavio Garcia Copyright 2016-2017 Veeti Paananen under MIT License Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/README.md b/README.md index cb4f2ff..0c58351 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![PyPI](https://img.shields.io/pypi/v/automatoes.svg)](https://pypi.org/project/automatoes/) [![Number of PyPI downloads](https://img.shields.io/pypi/dm/automatoes.svg)](https://pypi.org/project/automatoes/#files) [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fcandango%2Fautomatoes%2Fbadge&style=flat)](https://actions-badge.atrox.dev/candango/automatoes/goto) -[![Requirements Status](https://requires.io/github/candango/automatoes/requirements.svg?branch=develop)](https://requires.io/github/candango/automatoes/requirements/?branch=develop) Automatoes is a [Let's Encrypt](https://letsencrypt.org)/[ACME](https://github.com/ietf-wg-acme/acme/) @@ -16,7 +15,7 @@ original project and to be a direct replacement from ## Why? -Bacause Let's Encrypt's point is to be automatic and seamless and ManuaLE was +Because Let's Encrypt's point is to be automatic and seamless and ManuaLE was designed to be manual. Automatoes will add automatic workflows and new features to evolve ManuaLe's @@ -45,7 +44,7 @@ still bring your own keys and/or CSR's. Everybody wins. ## Installation -Python 3.6 or above is required. +Python 3.9 or above is required. ### Using your package manager @@ -200,7 +199,7 @@ this one will be deleted, and a new order will be created. > 1. /acme/cert/ is called, and we place keys like we use to do before > 1. we're done! -* If you try to issue certificates for a domain sequence and an oder is pending +* If you try to issue certificates for a domain sequence and an order is pending or invalid, automatoes will ask you to run authorize before. After authorizing a domain sequence you need run issue with the same domain @@ -291,5 +290,5 @@ initiatives. Available under the This website and all documentation are licensed under [Creative Commons 3.0](http://creativecommons.org/licenses/by/3.0/). -Copyright © 2019-2022 Flávio Gonçalves Garcia +Copyright © 2019-2025 Flavio Garcia Copyright © 2016-2017 Veeti Paananen under MIT License diff --git a/automatoes/__init__.py b/automatoes/__init__.py index e856551..abe432d 100644 --- a/automatoes/__init__.py +++ b/automatoes/__init__.py @@ -1,6 +1,6 @@ # -*- coding: UTF-8 -*- # -# Copyright 2019-2022 Flávio Gonçalves Garcia +# Copyright 2019-2025 Flavio Garcia # Copyright 2016-2017 Veeti Paananen under MIT License # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,7 @@ __author__ = "Flavio Garcia " -__version__ = (0, 9, 10) +__version__ = (0, 9, 13) __licence__ = "Apache License V2.0" diff --git a/automatoes/acme.py b/automatoes/acme.py index d47c77f..4ef90a2 100644 --- a/automatoes/acme.py +++ b/automatoes/acme.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python -# -# Copyright 2019-2020 Flavio Garcia +# Copyright 2019-2025 Flavio Garcia # Copyright 2016-2017 Veeti Paananen under MIT License # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,12 +17,13 @@ ACME API client. """ -from . import get_version -from .crypto import (export_certificate_for_acme, generate_header, jose_b64, - sign_request, sign_request_v2) -from .errors import AccountAlreadyExistsError, AcmeError -from .model import Order, Challenge -from collections import namedtuple +from automatoes import get_version +from automatoes.crypto import (export_certificate_for_acme, generate_header, + jose_b64, sign_request, sign_request_v2) +from automatoes.errors import AccountAlreadyExistsError, AcmeError +from automatoes.model import (Challenge, IssuanceResult, + NewAuthorizationResult, Order, + RegistrationResult) import copy import datetime import hashlib @@ -241,12 +240,6 @@ def path(self, path): return urljoin(self.url, path) -RegistrationResult = namedtuple("RegistrationResult", "contents uri terms") -NewAuthorizationResult = namedtuple("NewAuthorizationResult", "contents uri") -IssuanceResult = namedtuple("IssuanceResult", - "certificate location intermediate") - - class AcmeV2(Acme): def __init__(self, url, account, directory="directory", verify=None, @@ -413,8 +406,6 @@ def get_order_challenges(self, order: Order): :param order: order to be challenged :return: Order """ - domains = [identifier['value'] for identifier in - order.contents['identifiers']] order_challenges = [] for auth in order.contents['authorizations']: auth_response = _json(self.post_as_get(auth, self.account.uri)) @@ -522,7 +513,7 @@ def post(self, path, body, headers=None, kid=None): protected = self.get_headers(url=self.path(path)) if kid: protected['kid'] = kid - protected.pop('jwk') + protected.pop("jwk") body = sign_request_v2(self.account.key, protected, body) kwargs = { 'headers': _headers @@ -539,7 +530,7 @@ def post_as_get(self, path, kid, headers=None): protected = self.get_headers(url=self.path(path)) protected['kid'] = kid - protected.pop('jwk') + protected.pop("jwk") body = sign_request_v2(self.account.key, protected, None) kwargs = { 'headers': _headers diff --git a/automatoes/cli/__init__.py b/automatoes/cli/__init__.py index 74d6d71..179d6b6 100644 --- a/automatoes/cli/__init__.py +++ b/automatoes/cli/__init__.py @@ -87,6 +87,7 @@ def _issue(args): key_file=args.key_file, csr_file=args.csr_file, output_path=args.output, + output_filename=args.output_filename, must_staple=args.ocsp_must_staple, verbose=verbose ) @@ -228,6 +229,9 @@ def manuale_main(): issue_sub.add_argument('--output', '-o', help="The output directory for created objects", default='.') + issue_sub.add_argument('--output-filename', + help="The filename base for created objects", + default=None) issue_sub.add_argument('--ocsp-must-staple', dest='ocsp_must_staple', help="CSR: Request OCSP Must-Staple extension", diff --git a/automatoes/cli/commands/account.py b/automatoes/cli/commands/account.py index 7bbd22f..7bb5352 100644 --- a/automatoes/cli/commands/account.py +++ b/automatoes/cli/commands/account.py @@ -21,7 +21,7 @@ @taskio.group(name="account", short_help="Group with commands related to " - "account managment") + "account management") @pass_context def account(ctx): pass diff --git a/automatoes/cli/commands/order.py b/automatoes/cli/commands/order.py index 3a58ed8..add6f36 100644 --- a/automatoes/cli/commands/order.py +++ b/automatoes/cli/commands/order.py @@ -19,7 +19,7 @@ @taskio.group(name="order", short_help="Group with commands related to order " - "managment") + "management") @pass_context def order(ctx): pass diff --git a/automatoes/crypto.py b/automatoes/crypto.py index c8d7e0d..2d8db51 100644 --- a/automatoes/crypto.py +++ b/automatoes/crypto.py @@ -1,6 +1,4 @@ -# -*- coding: UTF-8 -*- -# -# Copyright 2019-2023 Flávio Gonçalves Garcia +# Copyright 2019-2025 Flavio Garcia # Copyright 2016-2017 Veeti Paananen under MIT License # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,12 +21,8 @@ import binascii import json import logging -# TODO: Remove that after python 3.5 depreciation -import warnings -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - from cryptography import x509 -from cryptography.x509 import NameOID +from cryptography import x509 +from cryptography.x509 import NameOID, DNSName from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric.rsa import ( @@ -90,6 +84,13 @@ def certbot_key_data_to_int(key_data: dict) -> dict: return key_data_int +def generate_ari_data(cert): + aki_b64 = base64.urlsafe_b64encode(get_certificate_aki(cert).encode()) + serial_b64 = base64.urlsafe_b64encode( + get_certificate_serial(cert).encode()) + return f"{aki_b64}.{serial_b64}" + + def generate_header(account_key): """ Creates a new request header for the specified account key. @@ -97,7 +98,7 @@ def generate_header(account_key): numbers = account_key.public_key().public_numbers() e = numbers.e.to_bytes((numbers.e.bit_length() // 8 + 1), byteorder='big') n = numbers.n.to_bytes((numbers.n.bit_length() // 8 + 1), byteorder='big') - if n[0] == 0: # for strict JWK + if n[0] == 0: # for strict JWK n = n[1:] return { 'alg': 'RS256', @@ -178,7 +179,8 @@ def export_private_key(key): """ Exports a private key in OpenSSL PEM format. """ - return key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()) + return key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, + NoEncryption()) def create_csr(key, domains, must_staple=False): @@ -228,8 +230,27 @@ def load_pem_certificate(data): return x509.load_pem_x509_certificate(data, default_backend()) +def get_issuer_certificate_domain_name(cert): + for cn in cert.subject: + return cn.value + + +def get_certificate_aki(cert): + for ext in cert.extensions: + if isinstance(ext.value, x509.AuthorityKeyIdentifier): + hex = ext.value.key_identifier.hex() + return ":".join(hex[i:i+2] for i in range(0, len(hex), 2)) + + +def get_certificate_serial(cert): + hex = format(cert.serial_number, "x") + return ":".join(hex[i:i+2] for i in range(0, len(hex), 2)) + + def get_certificate_domain_name(cert): - return cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value + for ext in cert.extensions: + if isinstance(ext.value, x509.SubjectAlternativeName): + return ext.value.get_values_for_type(DNSName)[0] def get_certificate_domains(cert): diff --git a/automatoes/issue.py b/automatoes/issue.py index b09ca34..d8c54da 100644 --- a/automatoes/issue.py +++ b/automatoes/issue.py @@ -50,7 +50,7 @@ def issue(server, paths, account, domains, key_size, key_file=None, - csr_file=None, output_path=None, must_staple=False, verbose=False): + csr_file=None, output_path=None, output_filename=None, must_staple=False, verbose=False): print("Candango Automatoes {}. Manuale replacement.\n\n".format( get_version())) @@ -195,11 +195,12 @@ def issue(server, paths, account, domains, key_size, key_file=None, # Write the key, certificate and full chain os.makedirs(output_path, exist_ok=True) - cert_path = os.path.join(output_path, domains[0] + '.crt') - chain_path = os.path.join(output_path, domains[0] + '.chain.crt') + cert_name = output_filename if output_filename else domains[0] + cert_path = os.path.join(output_path, cert_name + '.crt') + chain_path = os.path.join(output_path, cert_name + '.chain.crt') intermediate_path = os.path.join(output_path, - domains[0] + '.intermediate.crt') - key_path = os.path.join(output_path, domains[0] + '.pem') + cert_name + '.intermediate.crt') + key_path = os.path.join(output_path, cert_name + '.pem') if order.key is not None: with open(key_path, 'wb') as f: diff --git a/automatoes/model.py b/automatoes/model.py index 1999623..34bd141 100644 --- a/automatoes/model.py +++ b/automatoes/model.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python -# -# Copyright 2019-2022 Flávio Gonçalves Garcia +# Copyright 2019-2025 Flavio Garcia # Copyright 2016-2017 Veeti Paananen under MIT License # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +18,7 @@ from .crypto import (generate_jwk_thumbprint, load_private_key, export_private_key) +from collections import namedtuple from datetime import datetime @@ -141,3 +140,9 @@ def deserialize(data): return order except (TypeError, ValueError, AttributeError) as e: raise IOError("Invalid account structure: {}".format(e)) + + +RegistrationResult = namedtuple("RegistrationResult", "contents uri terms") +NewAuthorizationResult = namedtuple("NewAuthorizationResult", "contents uri") +IssuanceResult = namedtuple("IssuanceResult", + "certificate location intermediate") diff --git a/automatoes/protocol.py b/automatoes/protocol.py index 0abd282..88b58d9 100644 --- a/automatoes/protocol.py +++ b/automatoes/protocol.py @@ -1,6 +1,4 @@ -# -*- coding: UTF-8 -*- -# -# Copyright 2019-2022 Flávio Gonçalves Garcia +# Copyright 2019-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,82 +13,15 @@ # limitations under the License. from . import get_version -from peasant import get_version as peasant_get_version -from peasant.client import Peasant, PeasantTransport -import requests -from urllib.parse import urljoin, urlparse - - -class AcmeV2RequestsTransport(PeasantTransport): - - def __init__(self): - super().__init__() - self._directory = None - self._user_agent = ("Automat-o-es %s Peasant %s" % ( - get_version(), peasant_get_version())) - - @property - def DEFAULT_HEADERS(self) -> dict: - return {'User-Agent': self._user_agent} +from peasant.client.protocol import Peasant +from peasant.client.transport_requests import RequestsTransport - def set_directory(self): - response = self.get("/%s" % self.peasant.directory_path) - if response.status_code == 200: - self.peasant.directory_cache = response.json() - else: - raise Exception - def new_nonce(self): - """ Returns a new nonce """ - return self.head(self.peasant.directory['newNonce'], headers={ - 'resource': "new-reg", - 'payload': None, - }).headers.get('Replay-Nonce') - - def get(self, path, **kwargs): - kwargs = self.create_kwargs(**kwargs) - return requests.get(self.sanatize_path(path), **kwargs) - - def head(self, path, **kwargs): - kwargs = self.create_kwargs(**kwargs) - return requests.head(self.path(path), **kwargs) - - def create_kwargs(self, **kwargs): - _headers = self.DEFAULT_HEADERS.copy() - headers = kwargs.get("headers") - if headers: - _headers.update(headers) - kwargs = { - 'headers': _headers - } - if self.peasant.verify: - kwargs['verify'] = self.peasant.verify - return kwargs - - def sanatize_path(self, path): - """ Handle path and make it sure the right path will be used combined - with the url from set to the peasant. - """ - # If https is in we assume that was returned by the directory - if path.startswith("https"): - return path - # Make sure path is relative - if path.startswith("http"): - path = urlparse(path).path - url_parsed = urlparse(self.peasant.url) - if url_parsed.path != "": - url_parsed_path = url_parsed.path - if url_parsed_path.startswith("/"): - url_parsed_path = url_parsed_path[1:] - if path.startswith("/"): - path = path[1:] - path = "%s/%s" % (url_parsed_path, path) - return urljoin(self.peasant.url, path) - - -class AcmeProtocol(Peasant): +class AcmeV2Pesant(Peasant): def __init__(self, transport, **kwargs): + """ + """ super().__init__(transport) self._url = kwargs.get("url") self._account = kwargs.get("account") @@ -121,3 +52,36 @@ def directory_path(self, path): @property def verify(self): return self._verify + + +class AcmeRequestsTransport(RequestsTransport): + + peasant: AcmeV2Pesant + + def __init__(self, bastion_address): + super().__init__(bastion_address) + self._directory = None + self.user_agent = (f"Automatoes/{get_version()} {self.user_agent}") + self.basic_headers = { + 'User-Agent': self.user_agent + } + self.kwargs_updater = self.update_kwargs + + def update_kwargs(self, method, **kwargs): + if self.peasant.verify: + kwargs['verify'] = self.peasant.verify + return kwargs + + def set_directory(self): + response = self.get("/%s" % self.peasant.directory_path) + if response.status_code == 200: + self.peasant.directory_cache = response.json() + else: + raise Exception + + def new_nonce(self): + """ Returns a new nonce """ + return self.head(self.peasant.directory()['newNonce'], headers={ + 'resource': "new-reg", + 'payload': None, + }).headers.get('Replay-Nonce') diff --git a/docs/index.rst b/docs/index.rst index 20ff1b9..348c6e1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,7 @@ original project and to be a direct replacement from Why? ==== -Bacause Let's Encrypt's point is to be automatic and seamless and ManuaLE was +Because Let's Encrypt's point is to be automatic and seamless and ManuaLE was designed to be manual. Automatoes will add automatic workflows and new features to evolve ManuaLe's diff --git a/requirements/basic.txt b/requirements/basic.txt index d00a8d6..3fd3a96 100644 --- a/requirements/basic.txt +++ b/requirements/basic.txt @@ -1,3 +1,2 @@ -peasant==0.6 -requests==2.31.1 -taskio==0.0.5 +peasant[requests]>=0.7.5 +taskio>=0.0.7 diff --git a/requirements/tests.txt b/requirements/tests.txt index cda6a36..fee8d2a 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1 +1,2 @@ behave==1.2.6 +tornado==6.4.2 diff --git a/scripts/build.sh b/scripts/build.sh index 5e18e3e..7c51c26 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,6 +1,6 @@ #!/bin/bash ## -## Copyright 2019-2022 Flávio Gonçalves Garcia +## Copyright 2019-2025 Flavio Garcia ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. @@ -16,9 +16,8 @@ ## ## build.sh Build packages to be uploaded to pypi. ## -## Author: Flavio Gonçalves Garcia +## Author: Flavio Garcia -python setup.py bdist_wheel --universal -python setup.py sdist +python -m build rm -rf build rm -rf automatoes.egg-info diff --git a/setup.py b/setup.py index 5a2255f..1cb69e1 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright 2019-2021 Flavio Garcia +# Copyright 2019-2025 Flavio Garcia # Copyright 2016-2017 Veeti Paananen under MIT License # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,18 +16,8 @@ # limitations under the License. import automatoes -from codecs import open from setuptools import find_packages, setup -import sys - -try: - # for pip >= 10 - from pip._internal.req import parse_requirements -except ImportError: - # for pip <= 9.0.3 - print("error: Upgrade to a pip version newer than 10. Run \"pip install " - "--upgrade pip\".") - sys.exit(1) +import os with open("README.md", "r") as fh: long_description = fh.read() @@ -35,17 +25,21 @@ # Solution from http://bit.ly/29Yl8VN def resolve_requires(requirements_file): - try: - requirements = parse_requirements("./%s" % requirements_file, - session=False) - return [str(ir.req) for ir in requirements] - except AttributeError: - # for pip >= 20.1.x - # Need to run again as the first run was ruined by the exception - requirements = parse_requirements("./%s" % requirements_file, - session=False) - # pr stands for parsed_requirement - return [str(pr.requirement) for pr in requirements] + requires = [] + if os.path.isfile(f"./{requirements_file}"): + file_dir = os.path.dirname(f"./{requirements_file}") + with open(f"./{requirements_file}") as f: + for raw_line in f.readlines(): + line = raw_line.strip().replace("\n", "") + if len(line) > 0: + if line.startswith("-r "): + partial_file = os.path.join(file_dir, line.replace( + "-r ", "")) + partial_requires = resolve_requires(partial_file) + requires = requires + partial_requires + continue + requires.append(line) + return requires setup( @@ -59,6 +53,7 @@ def resolve_requires(requirements_file): url="https://github.com/candango/automatoes", author=automatoes.get_author(), author_email=automatoes.get_author_email(), + python_requires=">= 3.9", classifiers=[ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", @@ -67,11 +62,11 @@ def resolve_requires(requirements_file): "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", ], packages=find_packages(), diff --git a/tests/__init__.py b/tests/__init__.py index 0390a39..32abf81 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python -# -# Copyright 2019-2022 Flávio Gonçalves Garcia +# Copyright 2019-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,3 +18,9 @@ TEST_ROOT = os.path.dirname(os.path.abspath(__file__)) FIXTURES_ROOT = os.path.abspath(os.path.join(TEST_ROOT, "fixtures")) PROJECT_ROOT = os.path.abspath(os.path.join(TEST_ROOT, "..")) + + +def get_absolute_path(directory): + return os.path.realpath( + os.path.join(os.path.dirname(__file__), directory) + ) diff --git a/tests/ari_test.py b/tests/ari_test.py new file mode 100644 index 0000000..d9968ad --- /dev/null +++ b/tests/ari_test.py @@ -0,0 +1,53 @@ +# Copyright 2019-2025 Flavio Garcia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import FIXTURES_ROOT +from automatoes.crypto import (generate_ari_data, + get_certificate_aki, + get_certificate_serial, + load_pem_certificate) +import base64 +from cartola import fs +import unittest +import os + + +class ARITestCase(unittest.TestCase): + """ Tests the crypto module from automatoes + """ + + def test_aki(self): + """ Test the strip_certificate function """ + key_directory = os.path.join(FIXTURES_ROOT, "keys", "candango.org", + "another") + + key_crt = fs.read( + os.path.join(key_directory, "another.candango.org.crt"), + True + ) + + pem_crt = load_pem_certificate(key_crt) + + aki = get_certificate_aki(pem_crt) + serial = get_certificate_serial(pem_crt) + expected_aki = ("c0:cc:03:46:b9:58:20:cc:5c:72:70:f3:e1:2e:cb:20:a6:" + "f5:68:3a") + expected_serial = ("fa:f3:97:73:26:ea:e8:44:e7:14:00:20:ae:90:60:af:" + "ba:44") + aki_b64 = base64.urlsafe_b64encode(expected_aki.encode()) + serial_b64 = base64.urlsafe_b64encode(expected_serial.encode()) + + self.assertEqual(expected_aki, aki) + self.assertEqual(expected_serial, serial) + self.assertEqual(f"{aki_b64}.{serial_b64}", generate_ari_data(pem_crt)) diff --git a/tests/crypto_test.py b/tests/crypto_test.py index 824ee08..890904d 100644 --- a/tests/crypto_test.py +++ b/tests/crypto_test.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python -# -# Copyright 2019-2022 Flávio Gonçalves Garcia +# Copyright 2019-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/features/01_user_managment.feature b/tests/features/01_user_management.feature similarity index 100% rename from tests/features/01_user_managment.feature rename to tests/features/01_user_management.feature diff --git a/tests/features/environment.py b/tests/features/environment.py index 21a666b..caa8066 100644 --- a/tests/features/environment.py +++ b/tests/features/environment.py @@ -1,6 +1,4 @@ -# -*- coding: UTF-8 -*- -# -# Copyright 2019-2022 Flávio Gonçalves Garcia +# Copyright 2019-2024 Flavio Garcia # Copyright 2016-2017 Veeti Paananen under MIT License # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +17,7 @@ # https://community.letsencrypt.org/t/staging-endpoint-for-acme-v2/49605 from behave import fixture, use_fixture from automatoes.acme import AcmeV2 -from automatoes.protocol import AcmeProtocol, AcmeV2RequestsTransport +from automatoes.protocol import AcmeV2Pesant, AcmeRequestsTransport import os from unittest.case import TestCase @@ -34,8 +32,8 @@ def get_absolute_path(directory): @fixture def acme_protocol(context, timeout=1, **kwargs): - transport = AcmeV2RequestsTransport() - context.acme_protocol = AcmeProtocol( + transport = AcmeRequestsTransport(peeble_url) + context.acme_protocol = AcmeV2Pesant( transport, url=peeble_url, directory="dir", diff --git a/tests/features/steps/acme_v2_steps.py b/tests/features/steps/acme_v2_steps.py index 97dc70f..e5bd740 100644 --- a/tests/features/steps/acme_v2_steps.py +++ b/tests/features/steps/acme_v2_steps.py @@ -18,7 +18,8 @@ from behave import given, when, then from automatoes.crypto import (create_csr, generate_rsa_key, strip_certificates, load_pem_certificate, - get_certificate_domain_name) + get_certificate_domain_name, + get_issuer_certificate_domain_name) from automatoes.model import Account from cartola import security @@ -169,7 +170,7 @@ def step_order_has_a_certificate_with_domain(context, what_domain): ) issuer_certificate = load_pem_certificate(certificates[1]) context.tester.assertTrue( - get_certificate_domain_name( + get_issuer_certificate_domain_name( issuer_certificate ).startswith("Pebble Intermediate CA") ) diff --git a/tests/features/steps/migrate_steps.py b/tests/features/steps/migrate_steps.py index 537763b..1eb06f9 100644 --- a/tests/features/steps/migrate_steps.py +++ b/tests/features/steps/migrate_steps.py @@ -28,7 +28,7 @@ def key_int_to_data(value: int) -> str: - """ This is the invertion of what happens inside + """ This is the inversion of what happens inside automatoes.crypto.certbot_key_data_to_int """ hex_value = hex(value).replace("0x", "") diff --git a/tests/nonce_test.py b/tests/nonce_test.py new file mode 100644 index 0000000..957fd13 --- /dev/null +++ b/tests/nonce_test.py @@ -0,0 +1,32 @@ +# Copyright 2019-2024 Flavio Garcia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import get_absolute_path +from automatoes.protocol import AcmeV2Pesant, AcmeRequestsTransport +from tornado import testing + + +class NonceTestCase(testing.AsyncTestCase): + """ Test letsencrypt nonce + """ + + @testing.gen_test + async def test_auth(self): + transport = AcmeRequestsTransport("https://localhost:14000") + protocol = AcmeV2Pesant( + transport, + directory="dir", + verify=get_absolute_path("certs/candango.minica.pem") + ) + self.assertIsNotNone(protocol.new_nonce()) diff --git a/tests/runtests.py b/tests/runtests.py index 621949e..cf289a8 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright 2019-2022 Flávio Gonçalves Garcia +# Copyright 2019-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License.