From e1cd77a2ee6261105a99d156937401cd4df6878c Mon Sep 17 00:00:00 2001 From: Wolfgang Schnerring Date: Tue, 21 May 2024 17:14:40 +0200 Subject: [PATCH 1/9] Remove idna/pip workaround --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c7812b0..989d8fa 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ # # * https://github.com/zenhack/simp_le/issues/62 # * https://github.com/pypa/pip/issues/988 - 'idna<2.8', + 'idna', 'acme>=2.0,<3.0', 'cryptography', From 27e5d10887ecd3c7cbf09aa18d8a814b19a5aa42 Mon Sep 17 00:00:00 2001 From: Wolfgang Schnerring Date: Wed, 22 May 2024 15:41:39 +0200 Subject: [PATCH 2/9] Decrease default verbosity, for silent cronjobs --- simp_le.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simp_le.py b/simp_le.py index c7661cb..52e09b3 100755 --- a/simp_le.py +++ b/simp_le.py @@ -1538,7 +1538,7 @@ def revoke(args): def setup_logging(verbose): """Setup basic logging.""" - level = logging.DEBUG if verbose else logging.INFO + level = logging.DEBUG if verbose else logging.WARNING root_logger = logging.getLogger() root_logger.setLevel(level) handler = logging.StreamHandler() From 64fa504be24c26ce8b4bf4008a0e2669e18993fb Mon Sep 17 00:00:00 2001 From: Wolfgang Schnerring Date: Thu, 5 Dec 2024 19:23:18 +0100 Subject: [PATCH 3/9] Remove upper version bound on acme --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 989d8fa..ee15c26 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ # * https://github.com/pypa/pip/issues/988 'idna', - 'acme>=2.0,<3.0', + 'acme>=2.0', 'cryptography', # formerly known as acme.jose: 'josepy', From 0e68f64357443facd7d7a9b24f286ca2c907c025 Mon Sep 17 00:00:00 2001 From: Wolfgang Schnerring Date: Sun, 8 Jun 2025 08:52:51 +0200 Subject: [PATCH 4/9] Replace pkg_resources with stdlib --- simp_le.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/simp_le.py b/simp_le.py index 52e09b3..f9e3387 100755 --- a/simp_le.py +++ b/simp_le.py @@ -25,6 +25,7 @@ import datetime import doctest import hashlib +import importlib.metadata import errno import logging import os @@ -38,8 +39,6 @@ import traceback import unittest -import pkg_resources - import six from six.moves import zip # pylint: disable=redefined-builtin @@ -63,7 +62,7 @@ logger = logging.getLogger(__name__) # pylint: disable=invalid-name -VERSION = pkg_resources.require('simp_le-client')[0].version +VERSION = importlib.metadata.version('simp_le-client') URL = 'https://github.com/zenhack/simp_le' LE_PRODUCTION_URI = 'https://acme-v02.api.letsencrypt.org/directory' From 41ed19da042b2974679b248365b919abe31f47be Mon Sep 17 00:00:00 2001 From: Wolfgang Schnerring Date: Sun, 8 Jun 2025 08:54:22 +0200 Subject: [PATCH 5/9] Remove six --- setup.py | 1 - simp_le.py | 18 ++++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index ee15c26..d84eba8 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,6 @@ 'mock', 'pyOpenSSL', 'pytz', - 'six', ] tests_require = [ diff --git a/simp_le.py b/simp_le.py index f9e3387..70103eb 100755 --- a/simp_le.py +++ b/simp_le.py @@ -39,9 +39,6 @@ import traceback import unittest -import six -from six.moves import zip # pylint: disable=redefined-builtin - from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa @@ -62,7 +59,8 @@ logger = logging.getLogger(__name__) # pylint: disable=invalid-name -VERSION = importlib.metadata.version('simp_le-client') +# VERSION = importlib.metadata.version('simp_le-client') +VERSION = '1' URL = 'https://github.com/zenhack/simp_le' LE_PRODUCTION_URI = 'https://acme-v02.api.letsencrypt.org/directory' @@ -259,7 +257,7 @@ def decode(cls, data): try: utf8test = parts[0] - if isinstance(utf8test, six.binary_type): + if isinstance(utf8test, bytes): utf8test = utf8test.decode('utf-8') utf8test.encode('ascii') except UnicodeError: @@ -1090,7 +1088,7 @@ def compute_roots(vhosts, default_root): roots[vhost.name] = root empty_roots = dict((name, root) - for name, root in six.iteritems(roots) if root is None) + for name, root in roots.items() if root is None) if empty_roots: raise Error('Root for the following host(s) were not specified: {0}. ' 'Try --default_root or use -d example.com:/var/www/html ' @@ -1257,7 +1255,7 @@ def check_plugins_persist_all(ioplugins): not_persisted = { component - for component, persist in six.iteritems(persisted._asdict()) + for component, persist in persisted._asdict().items() if not persist } if not_persisted: @@ -1435,7 +1433,7 @@ def finalize_order(client, order, fetch_alternative_chains): def poll_and_answer(client, authorizations, roots): """Poll authorization status and answer challenge if required""" - for name, auth in six.iteritems(authorizations): + for name, auth in authorizations.items(): for _ in range(5): auth, _ = client.poll(auth) if auth.body.status == messages.STATUS_VALID: @@ -1481,7 +1479,7 @@ def persist_new_data(args, existing_data): for authorization in order.authorizations ) if any(supported_challb(auth) is None - for auth in six.itervalues(authorizations)): + for auth in authorizations.values()): raise Error('CA did not offer http-01-only challenge combo. ' 'This client is unable to solve any other challenges.') @@ -1517,7 +1515,7 @@ def persist_new_data(args, existing_data): )) raise error finally: - for name, auth in six.iteritems(authorizations): + for name, auth in authorizations.items(): challb = supported_challb(auth) remove_validation(roots[name], challb) From 183386cef75d87677ac5323030cf3b27fcee3c8f Mon Sep 17 00:00:00 2001 From: Wolfgang Schnerring Date: Sun, 8 Jun 2025 08:54:42 +0200 Subject: [PATCH 6/9] mock has moved to stdlib --- setup.py | 1 - simp_le.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d84eba8..ff338b6 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ 'cryptography', # formerly known as acme.jose: 'josepy', - 'mock', 'pyOpenSSL', 'pytz', ] diff --git a/simp_le.py b/simp_le.py index 70103eb..a610894 100755 --- a/simp_le.py +++ b/simp_le.py @@ -18,6 +18,7 @@ # along with this program. If not, see . # """Simple Let's Encrypt client.""" +from unittest import mock import abc import argparse import collections @@ -43,7 +44,6 @@ from cryptography.hazmat.primitives.asymmetric import rsa import josepy as jose -import mock import OpenSSL import pytz From 212de26b8759c860c080997dbf64e7a14130d542 Mon Sep 17 00:00:00 2001 From: Wolfgang Schnerring Date: Sun, 8 Jun 2025 08:55:43 +0200 Subject: [PATCH 7/9] Sort imports --- simp_le.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simp_le.py b/simp_le.py index a610894..20fa555 100755 --- a/simp_le.py +++ b/simp_le.py @@ -25,9 +25,9 @@ import contextlib import datetime import doctest +import errno import hashlib import importlib.metadata -import errno import logging import os import re From 27e9554fd2eb4f2980637dd1e763df3b29e05342 Mon Sep 17 00:00:00 2001 From: Wolfgang Schnerring Date: Sun, 8 Jun 2025 10:15:51 +0200 Subject: [PATCH 8/9] Replace pyopenssl with cryptography --- setup.py | 5 +- simp_le.py | 277 ++++++++++++++++------------------------------------- 2 files changed, 83 insertions(+), 199 deletions(-) diff --git a/setup.py b/setup.py index ff338b6..f5acfc6 100644 --- a/setup.py +++ b/setup.py @@ -19,12 +19,9 @@ # * https://github.com/pypa/pip/issues/988 'idna', - 'acme>=2.0', + 'acme>=4.0', 'cryptography', - # formerly known as acme.jose: 'josepy', - 'pyOpenSSL', - 'pytz', ] tests_require = [ diff --git a/simp_le.py b/simp_le.py index 20fa555..af49809 100755 --- a/simp_le.py +++ b/simp_le.py @@ -40,12 +40,12 @@ import traceback import unittest -from cryptography.hazmat.backends import default_backend +from cryptography import x509 +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization import josepy as jose -import OpenSSL -import pytz from acme import client as acme_client from acme import crypto_util @@ -59,8 +59,7 @@ logger = logging.getLogger(__name__) # pylint: disable=invalid-name -# VERSION = importlib.metadata.version('simp_le-client') -VERSION = '1' +VERSION = importlib.metadata.version('simp_le-client') URL = 'https://github.com/zenhack/simp_le' LE_PRODUCTION_URI = 'https://acme-v02.api.letsencrypt.org/directory' @@ -111,7 +110,7 @@ def gen_pkey(bits): """Generate a private key. >>> gen_pkey(1024) - + Args: bits: Bit size of the key. @@ -120,16 +119,15 @@ def gen_pkey(bits): Freshly generated private key. """ assert bits >= 1024 - pkey = OpenSSL.crypto.PKey() - pkey.generate_key(OpenSSL.crypto.TYPE_RSA, bits) - return pkey + return rsa.generate_private_key( + public_exponent=65537, key_size=bits) -def gen_csr(pkey, domains, sig_hash='sha256'): +def gen_csr(pkey, domains): """Generate a CSR. - >>> [str(domain) for domain in crypto_util._pyopenssl_cert_or_req_san( - ... gen_csr(gen_pkey(1024), [b'example.com', b'example.net']))] + >>> [str(domain) for domain in cert_or_req_san( + ... gen_csr(gen_pkey(1024), ['example.com', 'example.net']))] ['example.com', 'example.net'] Args: @@ -141,86 +139,11 @@ def gen_csr(pkey, domains, sig_hash='sha256'): Generated CSR. """ assert domains, 'Must provide one or more hostnames for the CSR.' - req = OpenSSL.crypto.X509Req() - req.add_extensions([ - OpenSSL.crypto.X509Extension( - b'subjectAltName', - critical=False, - value=b', '.join(b'DNS:' + d for d in domains) - ), - ]) - req.set_pubkey(pkey) - - req.set_version(0) - - req.sign(pkey, sig_hash) - return req - - -class ComparablePKey: # pylint: disable=too-few-public-methods - """Comparable key. - - Suppose you have the following keys with the same material: - - >>> pem = OpenSSL.crypto.dump_privatekey( - ... OpenSSL.crypto.FILETYPE_PEM, gen_pkey(1024)) - >>> k1 = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, pem) - >>> k2 = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, pem) - - Unfortunately, in pyOpenSSL, equality is not well defined: - - >>> k1 == k2 - False - - Using `ComparablePKey` you get the equality relation right: - - >>> ck1, ck2 = ComparablePKey(k1), ComparablePKey(k2) - >>> other_ckey = ComparablePKey(gen_pkey(1024)) - >>> ck1 == ck2 - True - >>> ck1 == k1 - False - >>> k1 == ck1 - False - >>> other_ckey == ck1 - False - - Non-equalty is also well defined: - - >>> ck1 != ck2 - False - >>> ck1 != k1 - True - >>> k1 != ck1 - True - >>> k1 != other_ckey - True - >>> other_ckey != ck1 - True - - Wrapepd key is available as well: - - >>> ck1.wrapped is k1 - True - - Internal implementation is not optimized for performance! - """ - - def __init__(self, wrapped): - self.wrapped = wrapped - - def __ne__(self, other): - return not self == other # pylint: disable=unneeded-not - - def _dump(self): - return OpenSSL.crypto.dump_privatekey( - OpenSSL.crypto.FILETYPE_ASN1, self.wrapped) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return NotImplemented - # pylint: disable=protected-access - return self._dump() == other._dump() + return x509.CertificateSigningRequestBuilder().subject_name( + x509.Name([]) + ).add_extension(x509.SubjectAlternativeName([ + x509.DNSName(x) for x in domains + ]), critical=False).sign(pkey, hashes.SHA256()) class Vhost(collections.namedtuple('Vhost', 'name root')): @@ -293,9 +216,9 @@ class IOPlugin: - for `account_key`: private account key, an instance of `acme.jose.JWK` - for `account_reg`: account registration info, an instance of `acme.messages.RegistrationResource` - - for `key`: private key, an instance of `OpenSSL.crypto.PKey` - - for `cert`: certificate, an instance of `OpenSSL.crypto.X509` - - for `chain`: certificate chain, a list of `OpenSSL.crypto.X509` instances + - for `key`: private key, an instance of `cryptography:RSAPrivateKey` + - for `cert`: certificate, an instance of `cryptography.x509.Certificate` + - for `chain`: certificate chain, a list of `cryptography.x509.Certifciate` instances """ EMPTY_DATA = Data( @@ -429,42 +352,29 @@ def dump_json(cls, json): return json.json_dumps() -class OpenSSLIOPlugin(IOPlugin): # pylint: disable=abstract-method - """IOPlugin that uses pyOpenSSL. - - Args: - typ: One of `OpenSSL.crypto.FILETYPE_*`, used in loading/dumping. - """ - - def __init__(self, typ=OpenSSL.crypto.FILETYPE_PEM, **kwargs): - self.typ = typ - super(OpenSSLIOPlugin, self).__init__(**kwargs) - +class CryptographyIOPlugin(IOPlugin): def load_key(self, data): - """Load private key.""" try: - key = OpenSSL.crypto.load_privatekey(self.typ, data) - except OpenSSL.crypto.Error: + return serialization.load_pem_private_key(data, None) + except Exception: raise Error("simp_le couldn't load a key from {0}; the " "file might be empty or corrupt.".format(self.path)) - return ComparablePKey(key) - def dump_key(self, data): - """Dump private key.""" - return OpenSSL.crypto.dump_privatekey(self.typ, data.wrapped).strip() + def dump_key(self, obj): + return obj.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption()) def load_cert(self, data): - """Load certificate.""" try: - cert = OpenSSL.crypto.load_certificate(self.typ, data) - except OpenSSL.crypto.Error: + return x509.load_pem_x509_certificate(data) + except Exception: raise Error("simp_le couldn't load a certificate from {0}; the " "file might be empty or corrupt.".format(self.path)) - return jose.ComparableX509(cert) - def dump_cert(self, data): - """Dump certificate.""" - return OpenSSL.crypto.dump_certificate(self.typ, data.wrapped).strip() + def dump_cert(self, obj): + return obj.public_bytes(serialization.Encoding.PEM) @IOPlugin.register(path='account_key.json') @@ -529,9 +439,8 @@ def save(self, data): return self.save_to_file(reg) -@IOPlugin.register(path='key.der', typ=OpenSSL.crypto.FILETYPE_ASN1) -@IOPlugin.register(path='key.pem', typ=OpenSSL.crypto.FILETYPE_PEM) -class KeyFile(FileIOPlugin, OpenSSLIOPlugin): +@IOPlugin.register(path='key.pem') +class KeyFile(FileIOPlugin, CryptographyIOPlugin): """Private key file plugin.""" def persisted(self): @@ -557,9 +466,8 @@ def save(self, data): return self.save_to_file(key) -@IOPlugin.register(path='cert.der', typ=OpenSSL.crypto.FILETYPE_ASN1) -@IOPlugin.register(path='cert.pem', typ=OpenSSL.crypto.FILETYPE_PEM) -class CertFile(FileIOPlugin, OpenSSLIOPlugin): +@IOPlugin.register(path='cert.pem') +class CertFile(FileIOPlugin, CryptographyIOPlugin): """Certificate file plugin.""" def persisted(self): @@ -585,8 +493,8 @@ def save(self, data): return self.save_to_file(cert) -@IOPlugin.register(path='chain.pem', typ=OpenSSL.crypto.FILETYPE_PEM) -class ChainFile(FileIOPlugin, OpenSSLIOPlugin): +@IOPlugin.register(path='chain.pem') +class ChainFile(FileIOPlugin, CryptographyIOPlugin): """Certificate chain plugin.""" def persisted(self): @@ -616,8 +524,8 @@ def save(self, data): return self.save_to_file(_PEMS_SEP.join(pems)) -@IOPlugin.register(path='fullchain.pem', typ=OpenSSL.crypto.FILETYPE_PEM) -class FullChainFile(FileIOPlugin, OpenSSLIOPlugin): +@IOPlugin.register(path='fullchain.pem') +class FullChainFile(FileIOPlugin, CryptographyIOPlugin): """Full chain file plugin.""" def persisted(self): @@ -649,8 +557,8 @@ def save(self, data): return self.save_to_file(_PEMS_SEP.join(pems)) -@IOPlugin.register(path='full.pem', typ=OpenSSL.crypto.FILETYPE_PEM) -class FullFile(FileIOPlugin, OpenSSLIOPlugin): +@IOPlugin.register(path='full.pem') +class FullFile(FileIOPlugin, CryptographyIOPlugin): """Private key, certificate and chain plugin.""" def persisted(self): @@ -756,18 +664,15 @@ class PluginIOTestMixin: def __init__(self, *args, **kwargs): super(PluginIOTestMixin, self).__init__(*args, **kwargs) - raw_key = gen_pkey(1024) + key = gen_pkey(1024) self.all_data = IOPlugin.Data( - account_key=jose.JWKRSA(key=rsa.generate_private_key( - public_exponent=65537, key_size=1024, - backend=default_backend(), - )), + account_key=jose.JWKRSA(key=gen_pkey(1024)), account_reg=messages.NewRegistration.from_data(), - key=ComparablePKey(raw_key), - cert=jose.ComparableX509(crypto_util.gen_ss_cert(raw_key, ['a'])), + key=key, + cert=crypto_util.make_self_signed_cert(key, ['a']), chain=[ - jose.ComparableX509(crypto_util.gen_ss_cert(raw_key, ['b'])), - jose.ComparableX509(crypto_util.gen_ss_cert(raw_key, ['c'])), + crypto_util.make_self_signed_cert(key, ['b']), + crypto_util.make_self_signed_cert(key, ['c']), ], ) self.key_data = IOPlugin.EMPTY_DATA._replace(key=self.all_data.key) @@ -820,6 +725,13 @@ class KeyFileTest(FileIOPluginTestMixin, UnitTestCase): # this is a test suite | pylint: disable=missing-docstring PLUGIN_CLS = KeyFile + def test_save_ignore_unpersisted(self): + # Can only compare RSAKey by comparing the raw bytes. + self.plugin.save(self.all_data) + saved = self.plugin.load().key + expected = self.all_data.key + self.assertEqual(self.plugin.dump_key(expected), self.plugin.dump_key(saved)) + class CertFileTest(FileIOPluginTestMixin, UnitTestCase): """Tests for CertFile.""" @@ -844,6 +756,16 @@ class FullFileTest(ChainFileIOPluginTestMixin, UnitTestCase): # this is a test suite | pylint: disable=missing-docstring PLUGIN_CLS = FullFile + def test_save_ignore_unpersisted(self): + self.plugin.save(self.all_data) + # Key save/load already tested in KeyFileTest. + saved = self.plugin.load()._replace(key=None) + expected = IOPlugin.Data( + *(data if persist else None for persist, data in + zip(self.plugin.persisted(), self.all_data))) + expected = expected._replace(key=None) + self.assertEqual(expected, saved) + class PortNumWarningTest(UnitTestCase): """Tests relating to the port number warning.""" @@ -1158,44 +1080,18 @@ def persist_data(args, existing_data, new_data): plugin.save(new_data) -def asn1_generalizedtime_to_dt(timestamp): - """Convert ASN.1 GENERALIZEDTIME to datetime. - - Useful for deserialization of `OpenSSL.crypto.X509.get_notAfter` and - `OpenSSL.crypto.X509.get_notAfter` outputs. - - TODO: Implement remaining two formats: *+hhmm, *-hhmm. - - >>> asn1_generalizedtime_to_dt('201511150803Z') - datetime.datetime(2015, 11, 15, 8, 0, 3, tzinfo=) - >>> asn1_generalizedtime_to_dt('201511150803+1512') - datetime.datetime(2015, 11, 15, 8, 0, 3, tzinfo=pytz.FixedOffset(912)) - >>> asn1_generalizedtime_to_dt('201511150803-1512') - datetime.datetime(2015, 11, 15, 8, 0, 3, tzinfo=pytz.FixedOffset(-912)) - """ - dt = datetime.datetime.strptime( # pylint: disable=invalid-name - timestamp[:12], '%Y%m%d%H%M%S') - if timestamp.endswith('Z'): - tzinfo = pytz.utc - else: - sign = -1 if timestamp[-5] == '-' else 1 - tzinfo = pytz.FixedOffset( - sign * (int(timestamp[-4:-2]) * 60 + int(timestamp[-2:]))) - return tzinfo.localize(dt) - - def renewal_necessary(cert, valid_min): """Is renewal necessary? - >>> cert = crypto_util.gen_ss_cert( - ... gen_pkey(1024), ['example.com'], validity=(60 *60)) + >>> cert = crypto_util.make_self_signed_cert( + ... gen_pkey(1024), ['example.com'], validity=datetime.timedelta(minutes=1)) >>> renewal_necessary(cert, 60 * 60 * 24) True >>> renewal_necessary(cert, 1) False """ - now = pytz.utc.localize(datetime.datetime.utcnow()) - expiry = asn1_generalizedtime_to_dt(cert.get_notAfter().decode()) + now = datetime.datetime.now(datetime.timezone.utc) + expiry = cert.not_valid_after_utc diff = expiry - now logger.debug('Certificate expires in %s on %s (relative to %s)', diff, expiry, now) @@ -1303,12 +1199,12 @@ def merge(first, second, field): return all_existing -def pyopenssl_cert_or_req_san(cert): - """SANs from cert or csr.""" - # This function is not inlined mainly because pylint is bugged - # when it comes to locally disabling protected access... - # pylint: disable=protected-access - return crypto_util._pyopenssl_cert_or_req_san(cert) +def cert_or_req_san(cert): + try: + san_ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) + return san_ext.value.get_values_for_type(x509.DNSName) + except x509.ExtensionNotFound: + return [] def valid_existing_cert(cert, vhosts, valid_min): @@ -1319,8 +1215,8 @@ def valid_existing_cert(cert, vhosts, valid_min): >>> valid_existing_cert(cert=None, vhosts=[], valid_min=0) False - >>> cert = jose.ComparableX509(crypto_util.gen_ss_cert( - ... gen_pkey(1024), ['example.com'], validity=(60 *60))) + >>> cert = crypto_util.make_self_signed_cert( + ... gen_pkey(1024), ['example.com'], validity=datetime.timedelta(minutes=1)) Return True iff `valid_min` is not bigger than certificate lifespan: @@ -1341,7 +1237,7 @@ def valid_existing_cert(cert, vhosts, valid_min): # renew existing? new_sans = [vhost.name for vhost in vhosts] - existing_sans = pyopenssl_cert_or_req_san(cert.wrapped) + existing_sans = cert_or_req_san(cert) logger.debug('Existing SANs: %r, new: %r', existing_sans, new_sans) return (set(existing_sans) == set(new_sans) and not renewal_necessary(cert, valid_min)) @@ -1353,8 +1249,7 @@ def check_or_generate_account_key(args, existing): logger.info('Generating new account key') return jose.JWKRSA(key=rsa.generate_private_key( public_exponent=args.account_key_public_exponent, - key_size=args.account_key_size, - backend=default_backend(), + key_size=args.account_key_size )) return existing @@ -1463,16 +1358,10 @@ def persist_new_data(args, existing_data): key = existing_data.key else: logger.info('Generating new certificate private key') - key = ComparablePKey(gen_pkey(args.cert_key_size)) - - csr = gen_csr( - key.wrapped, [vhost.name.encode() for vhost in args.vhosts] - ) - csr = OpenSSL.crypto.dump_certificate_request( - OpenSSL.crypto.FILETYPE_PEM, csr - ) + key = gen_pkey(args.cert_key_size) - order = client.new_order(csr) + csr = gen_csr(key, [vhost.name for vhost in args.vhosts]) + order = client.new_order(csr.public_bytes(serialization.Encoding.PEM)) authorizations = dict( [authorization.body.identifier.value, authorization] @@ -1497,11 +1386,9 @@ def persist_new_data(args, existing_data): account_key=client.net.key, account_reg=client.net.account, key=key, - cert=jose.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, pems[0])), + cert=x509.load_pem_x509_certificate(pems[0]), chain=[ - jose.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, pem)) + x509.load_pem_x509_certificate(pem) for pem in pems[1:] ], )) From c5b7c2b7b6d8294e31cd8c4dc4ed2761cd0f2fe1 Mon Sep 17 00:00:00 2001 From: Wolfgang Schnerring Date: Tue, 5 Aug 2025 17:50:59 +0200 Subject: [PATCH 9/9] Replace setup.py with pyproject.toml --- MANIFEST.in | 3 --- pyproject.toml | 30 ++++++++++++++++++++++ setup.py | 70 -------------------------------------------------- 3 files changed, 30 insertions(+), 73 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 979e7ca..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include README.rst -include CONTRIBUTING.md -include LICENSE.txt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4c56014 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "simp-le-client" +description = "Simple Let's Encrypt Client" +version = "2.0" + +dependencies = [ + "acme>=4.0", + "cryptography", + "josepy", +] +optional-dependencies = {test=[ + "pycodestyle", + "pylint", +]} + +authors = [{name="Ian Denhardt", email="ian@zenhack.net"}] +license = {text="GPLv3"} +urls = {Repository="https://github.com/wosc/simp_le"} +requires-python = ">=3.7" + +[project.scripts] +simp_le = "simp_le:main" + + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +include = ["simp_le.py"] \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index f5acfc6..0000000 --- a/setup.py +++ /dev/null @@ -1,70 +0,0 @@ -import codecs -import os -import setuptools - - -here = os.path.abspath(os.path.dirname(__file__)) -readme = codecs.open(os.path.join(here, 'README.rst'), encoding='utf-8').read() - -install_requires = [ - # We don't use idna directly, but problems with PyPI's solver - # have resulted in broken installations twice now, (when idna 2.6 - # was released, and again with 2.7) so as a workaround, we provide - # an explicit upper bound here, before any of the other constraints - # are read. - # - # See: - # - # * https://github.com/zenhack/simp_le/issues/62 - # * https://github.com/pypa/pip/issues/988 - 'idna', - - 'acme>=4.0', - 'cryptography', - 'josepy', -] - -tests_require = [ - 'pycodestyle', - 'pylint', -] - -setuptools.setup( - name='simp_le-client', - author='Ian Denhardt', - author_email='ian@zenhack.net', - description="Simple Let's Encrypt Client", - long_description=readme, - license='GPLv3', - url='https://github.com/zenhack/simp_le', - py_modules=['simp_le'], - setup_requires=['setuptools_scm'], - use_scm_version=True, - install_requires=install_requires, - extras_require={ - 'tests': tests_require, - }, - entry_points={ - 'console_scripts': [ - 'simp_le = simp_le:main', - ], - }, - classifiers=[ - 'Development Status :: 6 - Mature', - 'Environment :: Console', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Security', - 'Topic :: System :: Installation/Setup', - 'Topic :: System :: Networking', - 'Topic :: System :: Systems Administration', - 'Topic :: Utilities', - ], -)