franztls 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- franztls-0.1.4/LICENSE +21 -0
- franztls-0.1.4/PKG-INFO +68 -0
- franztls-0.1.4/README.md +24 -0
- franztls-0.1.4/pyproject.toml +38 -0
- franztls-0.1.4/setup.cfg +4 -0
- franztls-0.1.4/src/franztls/__init__.py +7 -0
- franztls-0.1.4/src/franztls/cert_manager.py +244 -0
- franztls-0.1.4/src/franztls/exceptions.py +2 -0
- franztls-0.1.4/src/franztls.egg-info/PKG-INFO +68 -0
- franztls-0.1.4/src/franztls.egg-info/SOURCES.txt +11 -0
- franztls-0.1.4/src/franztls.egg-info/dependency_links.txt +1 -0
- franztls-0.1.4/src/franztls.egg-info/requires.txt +7 -0
- franztls-0.1.4/src/franztls.egg-info/top_level.txt +1 -0
franztls-0.1.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alpamayo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
franztls-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: franztls
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Lightweight, open-source ACME client for automating TLS certificate issuance and renewal.
|
|
5
|
+
Author-email: Alpamayo <info@alpamayo-solutions.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Alpamayo
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/alpamayo-solutions/franztls
|
|
29
|
+
Project-URL: Issues, https://github.com/alpamayo-solutions/franztls/issues
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
34
|
+
Requires-Python: >=3.10
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: cryptography>=42.0
|
|
38
|
+
Requires-Dist: acme>=2.0
|
|
39
|
+
Requires-Dist: josepy>=1.13.0
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: build>=1.2.1; extra == "dev"
|
|
42
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# ๐ก๏ธ Franztls โ Simple ACME Certificate Manager for Python
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
`franztls` is a lightweight, open-source ACME client for automating TLS certificate issuance and renewal.
|
|
49
|
+
It works similarly to Let's Encrypt clients, but is designed for embedding directly in Python services or scripts.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## ๐ Features
|
|
54
|
+
|
|
55
|
+
- Fully self-contained ACME (RFC 8555) client
|
|
56
|
+
- Automatic certificate **issuance** and **renewal**
|
|
57
|
+
- Built-in **HTTP-01** challenge server
|
|
58
|
+
- Generates and stores **account**, **domain keys**, and **CSRs**
|
|
59
|
+
- Works with **local or public ACME servers**
|
|
60
|
+
- Minimal dependencies (`cryptography`, `acme`, `josepy`)
|
|
61
|
+
- Compatible with **Python โฅ3.10**
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## ๐ฆ Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install franztls
|
franztls-0.1.4/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ๐ก๏ธ Franztls โ Simple ACME Certificate Manager for Python
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
`franztls` is a lightweight, open-source ACME client for automating TLS certificate issuance and renewal.
|
|
5
|
+
It works similarly to Let's Encrypt clients, but is designed for embedding directly in Python services or scripts.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ๐ Features
|
|
10
|
+
|
|
11
|
+
- Fully self-contained ACME (RFC 8555) client
|
|
12
|
+
- Automatic certificate **issuance** and **renewal**
|
|
13
|
+
- Built-in **HTTP-01** challenge server
|
|
14
|
+
- Generates and stores **account**, **domain keys**, and **CSRs**
|
|
15
|
+
- Works with **local or public ACME servers**
|
|
16
|
+
- Minimal dependencies (`cryptography`, `acme`, `josepy`)
|
|
17
|
+
- Compatible with **Python โฅ3.10**
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ๐ฆ Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install franztls
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "franztls"
|
|
7
|
+
version = "0.1.4"
|
|
8
|
+
description = "Lightweight, open-source ACME client for automating TLS certificate issuance and renewal."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Alpamayo", email = "info@alpamayo-solutions.com" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"cryptography>=42.0",
|
|
15
|
+
"acme>=2.0",
|
|
16
|
+
"josepy>=1.13.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# These show up on PyPI
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/alpamayo-solutions/franztls"
|
|
29
|
+
Issues = "https://github.com/alpamayo-solutions/franztls/issues"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"build>=1.2.1",
|
|
37
|
+
"twine>=5.0.0",
|
|
38
|
+
]
|
franztls-0.1.4/setup.cfg
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import base64
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
import logging
|
|
6
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from cryptography import x509
|
|
11
|
+
from cryptography.x509.oid import NameOID
|
|
12
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
13
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
14
|
+
from cryptography.hazmat.backends import default_backend
|
|
15
|
+
from josepy import JWKRSA
|
|
16
|
+
from acme.client import ClientNetwork
|
|
17
|
+
from acme import client, messages, errors, challenges
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("CertManager")
|
|
21
|
+
if not logger.handlers:
|
|
22
|
+
logging.basicConfig(level=logging.INFO)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ReusableHTTPServer(HTTPServer):
|
|
26
|
+
allow_reuse_address = True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CertManager:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
service_id: str,
|
|
33
|
+
domain: str,
|
|
34
|
+
acme_directory: Optional[str] = "https://ca.localhost:9000/acme/acme/directory",
|
|
35
|
+
ca_file: Optional[str] = "/etc/certs/ca.crt",
|
|
36
|
+
account_key_path: Optional[str] = "/etc/certs/account.key",
|
|
37
|
+
domain_key_path: Optional[str] = "/etc/certs/domain.key",
|
|
38
|
+
csr_path: Optional[str] = "/etc/certs/domain.csr",
|
|
39
|
+
cert_path: Optional[str] = "/etc/certs/domain.pem",
|
|
40
|
+
renewal_buffer_hours: int = 24
|
|
41
|
+
) -> None:
|
|
42
|
+
self.service_id = service_id
|
|
43
|
+
self.domain = domain
|
|
44
|
+
self.acme_directory = acme_directory
|
|
45
|
+
self.ca_file = ca_file
|
|
46
|
+
|
|
47
|
+
self.account_key_path = account_key_path
|
|
48
|
+
self.domain_key_path = domain_key_path
|
|
49
|
+
self.csr_path = csr_path
|
|
50
|
+
self.cert_path = cert_path
|
|
51
|
+
|
|
52
|
+
self.challenge_responses = {}
|
|
53
|
+
self.httpd: Optional[HTTPServer] = None
|
|
54
|
+
self.thread: Optional[threading.Thread] = None
|
|
55
|
+
self._expiration_date: Optional[datetime] = None
|
|
56
|
+
|
|
57
|
+
self.acct_key = self._load_or_create_key(self.account_key_path)
|
|
58
|
+
self.jwkey = JWKRSA(key=self.acct_key)
|
|
59
|
+
self.client_net = ClientNetwork(
|
|
60
|
+
self.jwkey,
|
|
61
|
+
user_agent="franzTLS/1.0",
|
|
62
|
+
verify_ssl=self.ca_file
|
|
63
|
+
)
|
|
64
|
+
self.acme = self._connect_acme()
|
|
65
|
+
self.domain_key = self._load_or_create_key(self.domain_key_path)
|
|
66
|
+
self.renewal_buffer_hours = renewal_buffer_hours
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def cert_file(self) -> str:
|
|
70
|
+
return self.cert_path
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def key_file(self) -> str:
|
|
74
|
+
return self.domain_key_path
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def cert(self) -> Optional[bytes]:
|
|
78
|
+
if os.path.exists(self.cert_file):
|
|
79
|
+
with open(self.cert_file, "rb") as f:
|
|
80
|
+
return f.read()
|
|
81
|
+
logger.error(f"๐ No certificate found at {self.cert_file}. Do you still need to create it?")
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def key(self) -> Optional[bytes]:
|
|
86
|
+
if os.path.exists(self.key_file):
|
|
87
|
+
with open(self.key_file, "rb") as f:
|
|
88
|
+
return f.read()
|
|
89
|
+
logger.error(f"๐ No key found at {self.key_file}. Do you still need to create it?")
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def ca(self) -> str:
|
|
94
|
+
return self.ca_file
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def expiration_date(self) -> Optional[datetime]:
|
|
98
|
+
if self._expiration_date is None and self.cert:
|
|
99
|
+
try:
|
|
100
|
+
cert = x509.load_pem_x509_certificate(self.cert, default_backend())
|
|
101
|
+
self._expiration_date = cert.not_valid_after_utc
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.warning(f"โ ๏ธ Failed to parse expiration date: {e}")
|
|
104
|
+
return self._expiration_date
|
|
105
|
+
|
|
106
|
+
def _load_or_create_key(self, path: str):
|
|
107
|
+
if os.path.exists(path):
|
|
108
|
+
logger.debug(f"๐ Loading existing key from {path}")
|
|
109
|
+
with open(path, "rb") as f:
|
|
110
|
+
return serialization.load_pem_private_key(f.read(), password=None)
|
|
111
|
+
logger.info(f"๐ Generating new key at {path}")
|
|
112
|
+
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
113
|
+
with open(path, "wb") as f:
|
|
114
|
+
f.write(key.private_bytes(
|
|
115
|
+
encoding=serialization.Encoding.PEM,
|
|
116
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
117
|
+
encryption_algorithm=serialization.NoEncryption()))
|
|
118
|
+
return key
|
|
119
|
+
|
|
120
|
+
def _generate_csr(self) -> x509.CertificateSigningRequest:
|
|
121
|
+
logger.info("๐ Generating CSR...")
|
|
122
|
+
|
|
123
|
+
san_list = [
|
|
124
|
+
x509.DNSName(self.domain),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
csr_builder = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
|
|
128
|
+
x509.NameAttribute(NameOID.COMMON_NAME, self.domain),
|
|
129
|
+
])).add_extension(
|
|
130
|
+
x509.SubjectAlternativeName(san_list),
|
|
131
|
+
critical=False
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
csr = csr_builder.sign(self.domain_key, hashes.SHA256())
|
|
135
|
+
|
|
136
|
+
with open(self.csr_path, "wb") as f:
|
|
137
|
+
f.write(csr.public_bytes(serialization.Encoding.PEM))
|
|
138
|
+
return csr
|
|
139
|
+
|
|
140
|
+
def _connect_acme(self) -> client.ClientV2:
|
|
141
|
+
logger.info("๐ Connecting to ACME server...")
|
|
142
|
+
directory = messages.Directory.from_json(self.client_net.get(self.acme_directory).json())
|
|
143
|
+
acme = client.ClientV2(directory, self.client_net)
|
|
144
|
+
try:
|
|
145
|
+
acme.new_account(messages.NewRegistration.from_data(
|
|
146
|
+
terms_of_service_agreed=True,
|
|
147
|
+
email="admin@localhost"
|
|
148
|
+
))
|
|
149
|
+
logger.info("โ
Account registered.")
|
|
150
|
+
except errors.ConflictError as e:
|
|
151
|
+
logger.info("๐งพ Already registered, using existing account.")
|
|
152
|
+
acme.net.account = {"uri": e.location}
|
|
153
|
+
return acme
|
|
154
|
+
|
|
155
|
+
def _start_http_challenge_server(self) -> None:
|
|
156
|
+
class ChallengeHandler(BaseHTTPRequestHandler):
|
|
157
|
+
def do_GET(inner_self):
|
|
158
|
+
token = inner_self.path.split("/")[-1]
|
|
159
|
+
if token in self.challenge_responses:
|
|
160
|
+
inner_self.send_response(200)
|
|
161
|
+
inner_self.send_header("Content-Type", "text/plain")
|
|
162
|
+
inner_self.end_headers()
|
|
163
|
+
inner_self.wfile.write(self.challenge_responses[token].encode())
|
|
164
|
+
else:
|
|
165
|
+
inner_self.send_response(404)
|
|
166
|
+
inner_self.end_headers()
|
|
167
|
+
|
|
168
|
+
logger.debug("๐ Starting HTTP-01 challenge server on port 80...")
|
|
169
|
+
self.httpd = ReusableHTTPServer(("0.0.0.0", 80), ChallengeHandler)
|
|
170
|
+
self.thread = threading.Thread(target=self.httpd.serve_forever, daemon=True)
|
|
171
|
+
self.thread.start()
|
|
172
|
+
|
|
173
|
+
def _stop_http_challenge_server(self) -> None:
|
|
174
|
+
if self.httpd:
|
|
175
|
+
logger.info("๐ Stopping HTTP-01 challenge server...")
|
|
176
|
+
self.httpd.shutdown()
|
|
177
|
+
self.thread.join()
|
|
178
|
+
self.httpd.server_close() # Ensure socket is released
|
|
179
|
+
self.httpd = None
|
|
180
|
+
self.thread = None
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def needs_renewal(self) -> bool:
|
|
184
|
+
return self.expiration_date is None or datetime.now(timezone.utc) + timedelta(hours=self.renewal_buffer_hours) >= self.expiration_date
|
|
185
|
+
|
|
186
|
+
def renew_if_necessary(self) -> None:
|
|
187
|
+
exp = self.expiration_date
|
|
188
|
+
if exp is None:
|
|
189
|
+
logger.info("โณ No certificate found or unable to parse โ forcing renewal...")
|
|
190
|
+
self.force_renew()
|
|
191
|
+
return
|
|
192
|
+
if datetime.now(timezone.utc) + timedelta(hours=self.renewal_buffer_hours) >= exp:
|
|
193
|
+
logger.info(f"โณ Certificate expiring soon ({exp}) โ renewing...")
|
|
194
|
+
self.force_renew()
|
|
195
|
+
else:
|
|
196
|
+
logger.info(f"โ
Certificate is still valid until {exp}. No renewal needed.")
|
|
197
|
+
|
|
198
|
+
def force_renew(self) -> None:
|
|
199
|
+
logger.info("๐ Starting forced renewal...")
|
|
200
|
+
self.challenge_responses.clear()
|
|
201
|
+
csr = self._generate_csr()
|
|
202
|
+
self._start_http_challenge_server()
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
order = self.acme.new_order(csr.public_bytes(serialization.Encoding.PEM))
|
|
206
|
+
for authz in order.authorizations:
|
|
207
|
+
chall = next(c for c in authz.body.challenges if isinstance(c.chall, challenges.HTTP01))
|
|
208
|
+
token = (base64.urlsafe_b64encode(chall.token).decode("utf-8").rstrip("=")
|
|
209
|
+
if isinstance(chall.token, bytes) else chall.token)
|
|
210
|
+
response, validation = chall.response_and_validation(self.jwkey)
|
|
211
|
+
self.challenge_responses[token] = validation
|
|
212
|
+
|
|
213
|
+
logger.info(f"๐งช Starting challenge validation for {authz.body.identifier} and token {token}")
|
|
214
|
+
self.acme.answer_challenge(chall, response)
|
|
215
|
+
|
|
216
|
+
for _ in range(30):
|
|
217
|
+
status = self.acme.net.post(authz.uri, obj=None).json()["status"]
|
|
218
|
+
logger.debug(f"๐ Challenge status: {status}")
|
|
219
|
+
if status == "valid":
|
|
220
|
+
logger.info("โ
Challenge validated!")
|
|
221
|
+
break
|
|
222
|
+
elif status == "invalid":
|
|
223
|
+
logger.error("โ Challenge validation failed.")
|
|
224
|
+
raise Exception("Challenge validation failed.")
|
|
225
|
+
time.sleep(2)
|
|
226
|
+
else:
|
|
227
|
+
raise TimeoutError("โ Challenge validation timed out.")
|
|
228
|
+
|
|
229
|
+
logger.info("๐ Finalizing order...")
|
|
230
|
+
order = self.acme.poll_and_finalize(order)
|
|
231
|
+
with open(self.cert_file, "wb") as f:
|
|
232
|
+
f.write(order.fullchain_pem.encode())
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
self._expiration_date = x509.load_pem_x509_certificate(
|
|
236
|
+
order.fullchain_pem.encode(), default_backend()
|
|
237
|
+
).not_valid_after_utc
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.warning(f"โ ๏ธ Could not parse newly issued cert expiration: {e}")
|
|
240
|
+
|
|
241
|
+
logger.info(f"โ
Certificate saved to {self.cert_file}")
|
|
242
|
+
|
|
243
|
+
finally:
|
|
244
|
+
self._stop_http_challenge_server()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: franztls
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Lightweight, open-source ACME client for automating TLS certificate issuance and renewal.
|
|
5
|
+
Author-email: Alpamayo <info@alpamayo-solutions.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Alpamayo
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/alpamayo-solutions/franztls
|
|
29
|
+
Project-URL: Issues, https://github.com/alpamayo-solutions/franztls/issues
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
34
|
+
Requires-Python: >=3.10
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: cryptography>=42.0
|
|
38
|
+
Requires-Dist: acme>=2.0
|
|
39
|
+
Requires-Dist: josepy>=1.13.0
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: build>=1.2.1; extra == "dev"
|
|
42
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# ๐ก๏ธ Franztls โ Simple ACME Certificate Manager for Python
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
`franztls` is a lightweight, open-source ACME client for automating TLS certificate issuance and renewal.
|
|
49
|
+
It works similarly to Let's Encrypt clients, but is designed for embedding directly in Python services or scripts.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## ๐ Features
|
|
54
|
+
|
|
55
|
+
- Fully self-contained ACME (RFC 8555) client
|
|
56
|
+
- Automatic certificate **issuance** and **renewal**
|
|
57
|
+
- Built-in **HTTP-01** challenge server
|
|
58
|
+
- Generates and stores **account**, **domain keys**, and **CSRs**
|
|
59
|
+
- Works with **local or public ACME servers**
|
|
60
|
+
- Minimal dependencies (`cryptography`, `acme`, `josepy`)
|
|
61
|
+
- Compatible with **Python โฅ3.10**
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## ๐ฆ Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install franztls
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/franztls/__init__.py
|
|
5
|
+
src/franztls/cert_manager.py
|
|
6
|
+
src/franztls/exceptions.py
|
|
7
|
+
src/franztls.egg-info/PKG-INFO
|
|
8
|
+
src/franztls.egg-info/SOURCES.txt
|
|
9
|
+
src/franztls.egg-info/dependency_links.txt
|
|
10
|
+
src/franztls.egg-info/requires.txt
|
|
11
|
+
src/franztls.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
franztls
|