certapi 0.2.5__tar.gz → 0.2.8__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.
- {certapi-0.2.5 → certapi-0.2.8}/PKG-INFO +2 -2
- {certapi-0.2.5 → certapi-0.2.8}/README.md +1 -1
- {certapi-0.2.5 → certapi-0.2.8}/setup.py +1 -1
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/Acme.py +11 -4
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/certauthority.py +19 -8
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/crypto.py +7 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/db.py +48 -21
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/digitalocean_challenge_store.py +1 -1
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/digitalocean_client.py +5 -6
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi.egg-info/PKG-INFO +2 -2
- {certapi-0.2.5 → certapi-0.2.8}/MANIFEST.in +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/pyproject.toml +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/setup.cfg +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/__init__.py +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/challenge.py +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/cloudflare_challenge_store.py +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/cloudflare_client.py +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/crypto_classes.py +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/custom_certauthority.py +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi/util.py +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi.egg-info/SOURCES.txt +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi.egg-info/requires.txt +0 -0
- {certapi-0.2.5 → certapi-0.2.8}/src/certapi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: certapi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Python Package for managing keys, request SSL certificates from ACME.
|
|
5
5
|
Home-page: https://github.com/mesudip/certapi
|
|
6
6
|
Author: Sudip Bhattarai
|
|
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
# CertApi
|
|
15
15
|
|
|
16
16
|
CertApi is a Python package for requesting SSL certificates from ACME.
|
|
17
|
-
This is
|
|
17
|
+
This is to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
|
|
18
18
|
|
|
19
19
|
> ⚠️ Warning: This project is not polished for production use. Please stay tuned for the LTS `v1.0.0` release.
|
|
20
20
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CertApi
|
|
2
2
|
|
|
3
3
|
CertApi is a Python package for requesting SSL certificates from ACME.
|
|
4
|
-
This is
|
|
4
|
+
This is to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
|
|
5
5
|
|
|
6
6
|
> ⚠️ Warning: This project is not polished for production use. Please stay tuned for the LTS `v1.0.0` release.
|
|
7
7
|
|
|
@@ -72,9 +72,12 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
|
|
|
72
72
|
error["dns"] = {"error": "DNS record doesn't exist"}
|
|
73
73
|
error["hostname"] = re.findall(r"looking up [A-Z]+ for ([\w.-]+)", err_detail)[0]
|
|
74
74
|
message = error["hostname"] + " doesn't have a valid DNS record"
|
|
75
|
-
|
|
75
|
+
elif err_detail:
|
|
76
76
|
error["dns"] = {"error": err_detail}
|
|
77
77
|
message = err_detail
|
|
78
|
+
else:
|
|
79
|
+
error["dns"] = {"error": error}
|
|
80
|
+
message = err_detail
|
|
78
81
|
else:
|
|
79
82
|
validation_record = validation_record[0]
|
|
80
83
|
error["hostname"] = validation_record["hostname"]
|
|
@@ -103,9 +106,11 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
|
|
|
103
106
|
+ validation_record["port"]
|
|
104
107
|
+ "] Connection Refused (Is http server running?)"
|
|
105
108
|
)
|
|
106
|
-
|
|
109
|
+
elif err_detail:
|
|
107
110
|
message = err_detail
|
|
108
|
-
|
|
111
|
+
else:
|
|
112
|
+
message = error
|
|
113
|
+
elif err_detail:
|
|
109
114
|
pattern = r'Invalid response from .*?: "(.*)"'
|
|
110
115
|
|
|
111
116
|
match = re.search(pattern, err_detail)
|
|
@@ -121,6 +126,8 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
|
|
|
121
126
|
)
|
|
122
127
|
else:
|
|
123
128
|
message = err_detail
|
|
129
|
+
else:
|
|
130
|
+
message = error
|
|
124
131
|
|
|
125
132
|
if message is None:
|
|
126
133
|
if res_json.get("detail"):
|
|
@@ -338,7 +345,7 @@ class Order:
|
|
|
338
345
|
self._data["certificate"], step="Get Certificate from Successful Order"
|
|
339
346
|
)
|
|
340
347
|
certificate = crypto.x509.load_pem_x509_certificates(certificate_res.content)
|
|
341
|
-
return (
|
|
348
|
+
return (certificate_res.content, certificate)
|
|
342
349
|
|
|
343
350
|
def finalize(self, csr: CertificateSigningRequest):
|
|
344
351
|
"""
|
|
@@ -17,7 +17,7 @@ from requests import Response
|
|
|
17
17
|
from . import Acme, Challenge, Order
|
|
18
18
|
from . import crypto
|
|
19
19
|
from . import challenge
|
|
20
|
-
from .crypto import cert_to_pem, key_to_pem, digest_sha256
|
|
20
|
+
from .crypto import cert_to_pem, certs_to_pem, key_to_pem, digest_sha256
|
|
21
21
|
from .crypto_classes import Key
|
|
22
22
|
from .db import KeyStore
|
|
23
23
|
from .util import b64_string
|
|
@@ -121,9 +121,9 @@ class CertAuthority:
|
|
|
121
121
|
order.refresh() # is this refresh necessary?
|
|
122
122
|
|
|
123
123
|
if order.status == "valid":
|
|
124
|
-
fullchain_cert,certificate =
|
|
124
|
+
fullchain_cert, certificate = order.get_certificate()
|
|
125
125
|
key_id = self.key_store.save_key(private_key, missing[0])
|
|
126
|
-
cert_id = self.key_store.save_cert(key_id, certificate
|
|
126
|
+
cert_id = self.key_store.save_cert(key_id, certificate, missing)
|
|
127
127
|
issued_cert = IssuedCert(key_to_pem(private_key), fullchain_cert, missing)
|
|
128
128
|
# Clean up challenges after successful certificate issuance
|
|
129
129
|
for c in challenges:
|
|
@@ -145,7 +145,9 @@ class CertAuthority:
|
|
|
145
145
|
return createExistingResponse(existing, [])
|
|
146
146
|
|
|
147
147
|
|
|
148
|
-
def createExistingResponse(
|
|
148
|
+
def createExistingResponse(
|
|
149
|
+
existing: Dict[str, Tuple[int | str, Key, List[Certificate] | str]], issued_certs: List["IssuedCert"]
|
|
150
|
+
):
|
|
149
151
|
certs = []
|
|
150
152
|
certMap = {}
|
|
151
153
|
|
|
@@ -153,7 +155,13 @@ def createExistingResponse(existing: Dict[str, Tuple[int | str, Key, Certificate
|
|
|
153
155
|
if id in certMap:
|
|
154
156
|
certMap[id][0].append(h)
|
|
155
157
|
else:
|
|
156
|
-
|
|
158
|
+
if isinstance(cert, str):
|
|
159
|
+
cert_pem = cert
|
|
160
|
+
elif isinstance(cert, list):
|
|
161
|
+
cert_pem = certs_to_pem(cert).decode("utf-8")
|
|
162
|
+
else:
|
|
163
|
+
cert_pem = cert_to_pem(cert).decode("utf-8")
|
|
164
|
+
|
|
157
165
|
certMap[id] = (
|
|
158
166
|
[h],
|
|
159
167
|
key.to_pem().decode("utf-8"),
|
|
@@ -166,7 +174,6 @@ def createExistingResponse(existing: Dict[str, Tuple[int | str, Key, Certificate
|
|
|
166
174
|
return CertificateResponse(certs, issued_certs)
|
|
167
175
|
|
|
168
176
|
|
|
169
|
-
|
|
170
177
|
class CertificateResponse:
|
|
171
178
|
def __init__(self, existing, issued):
|
|
172
179
|
self.existing: List[IssuedCert] = existing
|
|
@@ -189,15 +196,19 @@ class CertificateResponse:
|
|
|
189
196
|
|
|
190
197
|
|
|
191
198
|
class IssuedCert:
|
|
192
|
-
def __init__(self, key: str | Key, cert: str | Certificate, domains: [str]):
|
|
199
|
+
def __init__(self, key: str | Key, cert: str | Certificate | List[Certificate], domains: [str]):
|
|
193
200
|
if isinstance(key, Key):
|
|
194
201
|
key = key.to_pem().decode("utf-8")
|
|
195
202
|
elif isinstance(key, bytes):
|
|
196
203
|
key = key.decode("utf-8")
|
|
197
|
-
|
|
204
|
+
|
|
205
|
+
if isinstance(cert, list):
|
|
206
|
+
cert = certs_to_pem(cert).decode("utf-8")
|
|
207
|
+
elif isinstance(cert, Certificate):
|
|
198
208
|
cert = cert_to_pem(cert).decode("utf-8")
|
|
199
209
|
elif isinstance(cert, bytes):
|
|
200
210
|
cert = cert.decode("utf-8")
|
|
211
|
+
|
|
201
212
|
self.privateKey = key
|
|
202
213
|
self.certificate = cert
|
|
203
214
|
self.domains = domains
|
|
@@ -106,16 +106,23 @@ def key_from_pem(data: bytes):
|
|
|
106
106
|
def cert_from_pem(data: bytes) -> Certificate:
|
|
107
107
|
return x509.load_pem_x509_certificate(data)
|
|
108
108
|
|
|
109
|
+
|
|
109
110
|
def certs_from_pem(data: bytes) -> List[Certificate]:
|
|
110
111
|
return x509.load_pem_x509_certificates(data)
|
|
111
112
|
|
|
113
|
+
|
|
112
114
|
def cert_to_der(cert):
|
|
113
115
|
return cert.public_bytes(serialization.Encoding.DER)
|
|
114
116
|
|
|
117
|
+
|
|
115
118
|
def cert_to_pem(cert):
|
|
116
119
|
return cert.public_bytes(serialization.Encoding.PEM)
|
|
117
120
|
|
|
118
121
|
|
|
122
|
+
def certs_to_pem(certs: List[Certificate]) -> bytes:
|
|
123
|
+
return b"".join([cert_to_pem(cert) for cert in certs])
|
|
124
|
+
|
|
125
|
+
|
|
119
126
|
def cert_from_der(data: bytes) -> Certificate:
|
|
120
127
|
return x509.load_der_x509_certificate(data)
|
|
121
128
|
|
|
@@ -21,11 +21,13 @@ class KeyStore(ABC):
|
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
23
|
@abstractmethod
|
|
24
|
-
def save_cert(
|
|
24
|
+
def save_cert(
|
|
25
|
+
self, private_key_id: int, cert: Certificate | str | List[Certificate], domains: List[str], name: str = None
|
|
26
|
+
) -> int:
|
|
25
27
|
pass
|
|
26
28
|
|
|
27
29
|
@abstractmethod
|
|
28
|
-
def get_cert(self, domain: str) -> None | Tuple[int | str, Key, Certificate]:
|
|
30
|
+
def get_cert(self, domain: str) -> None | Tuple[int | str, Key, Certificate | List[Certificate]]:
|
|
29
31
|
pass
|
|
30
32
|
|
|
31
33
|
|
|
@@ -91,12 +93,22 @@ class SqliteKeyStore(KeyStore):
|
|
|
91
93
|
self.save_key(key, name)
|
|
92
94
|
return key
|
|
93
95
|
|
|
94
|
-
def save_cert(
|
|
96
|
+
def save_cert(
|
|
97
|
+
self, private_key_id: int, cert: Certificate | str | List[Certificate], domains: List[str], name: str = None
|
|
98
|
+
) -> int:
|
|
95
99
|
conn = self._get_db_connection()
|
|
96
100
|
cur = conn.cursor()
|
|
101
|
+
|
|
102
|
+
if isinstance(cert, list):
|
|
103
|
+
cert_data = certs_to_pem(cert)
|
|
104
|
+
elif isinstance(cert, str):
|
|
105
|
+
cert_data = cert.encode()
|
|
106
|
+
else:
|
|
107
|
+
cert_data = cert_to_pem(cert)
|
|
108
|
+
|
|
97
109
|
cur.execute(
|
|
98
110
|
"INSERT INTO certificates (name, priv_id, content) VALUES (?, ?, ?)",
|
|
99
|
-
(name, private_key_id,
|
|
111
|
+
(name, private_key_id, cert_data),
|
|
100
112
|
)
|
|
101
113
|
cert_id = cur.lastrowid
|
|
102
114
|
|
|
@@ -106,7 +118,7 @@ class SqliteKeyStore(KeyStore):
|
|
|
106
118
|
conn.commit()
|
|
107
119
|
return cert_id
|
|
108
120
|
|
|
109
|
-
def get_cert(self, domain: str) -> None | Tuple[int | str, Key, Certificate]:
|
|
121
|
+
def get_cert(self, domain: str) -> None | Tuple[int | str, Key, List[Certificate]]:
|
|
110
122
|
conn = self._get_db_connection()
|
|
111
123
|
cur = conn.cursor()
|
|
112
124
|
cur.execute(
|
|
@@ -123,7 +135,11 @@ class SqliteKeyStore(KeyStore):
|
|
|
123
135
|
|
|
124
136
|
cur.close()
|
|
125
137
|
|
|
126
|
-
|
|
138
|
+
if res is None:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
certs = certs_from_pem(res[2])
|
|
142
|
+
return (res[0], Key.from_der(res[1]), certs)
|
|
127
143
|
|
|
128
144
|
def _init_account_key(self) -> RSAPrivateKey:
|
|
129
145
|
acme_key_name = "ACME Account Key"
|
|
@@ -174,20 +190,23 @@ class FilesystemKeyStore(KeyStore):
|
|
|
174
190
|
return key_from_pem(key_data)
|
|
175
191
|
return None
|
|
176
192
|
|
|
177
|
-
def find_cert(self, name: str) -> Union[None, Certificate]:
|
|
193
|
+
def find_cert(self, name: str) -> Union[None, List[Certificate]]:
|
|
178
194
|
cert_path = os.path.join(self.certs_dir, f"{name}.crt")
|
|
179
195
|
if os.path.exists(cert_path):
|
|
180
196
|
with open(cert_path, "rb") as f:
|
|
181
197
|
cert_data = f.read()
|
|
182
|
-
return
|
|
198
|
+
return certs_from_pem(cert_data)
|
|
183
199
|
return None
|
|
184
200
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
201
|
+
def save_cert(
|
|
202
|
+
self, private_key_id: str, cert: Certificate | str | List[Certificate], domains: list, name: str = None
|
|
203
|
+
) -> int:
|
|
204
|
+
if isinstance(cert, list):
|
|
205
|
+
cert_pem = certs_to_pem(cert)
|
|
206
|
+
elif isinstance(cert, str):
|
|
207
|
+
cert_pem = cert.encode()
|
|
208
|
+
else:
|
|
209
|
+
cert_pem = cert_to_pem(cert)
|
|
191
210
|
|
|
192
211
|
if name:
|
|
193
212
|
cert_path = os.path.join(self.certs_dir, f"{name}.crt")
|
|
@@ -212,10 +231,9 @@ class FilesystemKeyStore(KeyStore):
|
|
|
212
231
|
with open(domain_cert_path, "wb") as f:
|
|
213
232
|
f.write(cert_pem)
|
|
214
233
|
|
|
215
|
-
|
|
216
234
|
return name if name else domains[0] # Dummy ID since filesystem does not use numeric IDs
|
|
217
235
|
|
|
218
|
-
def get_cert(self, name: str) -> None | Tuple[str, Key, Certificate]:
|
|
236
|
+
def get_cert(self, name: str) -> None | Tuple[str, Key, List[Certificate]]:
|
|
219
237
|
cert_path = os.path.join(self.certs_dir, f"{name}.crt")
|
|
220
238
|
key_path = os.path.join(self.keys_dir, f"{name}.key")
|
|
221
239
|
key = None
|
|
@@ -230,7 +248,7 @@ class FilesystemKeyStore(KeyStore):
|
|
|
230
248
|
if os.path.exists(cert_path):
|
|
231
249
|
try:
|
|
232
250
|
with open(cert_path, "rb") as f:
|
|
233
|
-
cert =
|
|
251
|
+
cert = certs_from_pem(f.read())
|
|
234
252
|
except ValueError:
|
|
235
253
|
pass
|
|
236
254
|
|
|
@@ -329,13 +347,22 @@ class PostgresKeyStore(KeyStore):
|
|
|
329
347
|
self.save_key(key, name)
|
|
330
348
|
return key
|
|
331
349
|
|
|
332
|
-
def save_cert(
|
|
350
|
+
def save_cert(
|
|
351
|
+
self, private_key_id: int, cert: Certificate | str | List[Certificate], domains: List[str], name: str = None
|
|
352
|
+
) -> int:
|
|
333
353
|
"""Saves a certificate along with associated domains."""
|
|
334
354
|
with self.get_connection() as conn:
|
|
355
|
+
if isinstance(cert, list):
|
|
356
|
+
cert_data = certs_to_pem(cert)
|
|
357
|
+
elif isinstance(cert, str):
|
|
358
|
+
cert_data = cert.encode()
|
|
359
|
+
else:
|
|
360
|
+
cert_data = cert_to_pem(cert)
|
|
361
|
+
|
|
335
362
|
# Insert certificate and associated domains directly
|
|
336
363
|
conn.execute(
|
|
337
364
|
"INSERT INTO certificates (name, priv_id, content) VALUES (%s, %s, %s) RETURNING id",
|
|
338
|
-
(name, private_key_id,
|
|
365
|
+
(name, private_key_id, cert_data),
|
|
339
366
|
)
|
|
340
367
|
cert_id = conn.fetchone()[0]
|
|
341
368
|
|
|
@@ -345,7 +372,7 @@ class PostgresKeyStore(KeyStore):
|
|
|
345
372
|
|
|
346
373
|
return cert_id
|
|
347
374
|
|
|
348
|
-
def get_cert(self, domain: str) -> Optional[Tuple[int, RSAPrivateKey, Certificate]]:
|
|
375
|
+
def get_cert(self, domain: str) -> Optional[Tuple[int, RSAPrivateKey, List[Certificate]]]:
|
|
349
376
|
"""Fetches a certificate and its associated private key for a domain."""
|
|
350
377
|
with self.get_connection() as conn:
|
|
351
378
|
# Directly execute and fetch result
|
|
@@ -362,7 +389,7 @@ class PostgresKeyStore(KeyStore):
|
|
|
362
389
|
res = conn.fetchone()
|
|
363
390
|
|
|
364
391
|
if res:
|
|
365
|
-
return (res[0], Key.from_der(res[1]),
|
|
392
|
+
return (res[0], Key.from_der(res[1]), certs_from_pem(res[2]))
|
|
366
393
|
return None
|
|
367
394
|
|
|
368
395
|
def _init_account_key(self) -> RSAPrivateKey:
|
|
@@ -34,7 +34,7 @@ class DigitalOceanChallengeStore(ChallengeStore):
|
|
|
34
34
|
records = self.digitalocean.list_records(base_domain, name_filter=key)
|
|
35
35
|
for record in records:
|
|
36
36
|
if record["name"] == key:
|
|
37
|
-
return record["data"]
|
|
37
|
+
return record["data"] # DigitalOcean API returns 'data' for TXT record content
|
|
38
38
|
return None # Return None if not found, as per ChallengeStore's __getitem__ behavior
|
|
39
39
|
|
|
40
40
|
def delete_challenge(self, key: str, domain: str):
|
|
@@ -4,10 +4,9 @@ from os import getenv
|
|
|
4
4
|
from urllib.request import urlopen, Request # Python 3
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
class DigitalOcean(object):
|
|
9
|
-
def __init__(self,api_key=None):
|
|
10
|
-
self.token=api_key
|
|
8
|
+
def __init__(self, api_key=None):
|
|
9
|
+
self.token = api_key
|
|
11
10
|
self.api = "https://api.digitalocean.com/v2/domains"
|
|
12
11
|
if not self.token:
|
|
13
12
|
self.token = getenv("DIGITALOCEAN_API_TOKEN")
|
|
@@ -75,12 +74,12 @@ class DigitalOcean(object):
|
|
|
75
74
|
response = urlopen(Request(api, headers=request_headers))
|
|
76
75
|
if response.getcode() != 200:
|
|
77
76
|
raise Exception(json.loads(response.read().decode("utf8")))
|
|
78
|
-
|
|
77
|
+
|
|
79
78
|
all_records = json.loads(response.read().decode("utf8"))["domain_records"]
|
|
80
|
-
|
|
79
|
+
|
|
81
80
|
if name_filter:
|
|
82
81
|
filtered_records = [r for r in all_records if r["name"] == name_filter and r["type"] == "TXT"]
|
|
83
82
|
else:
|
|
84
83
|
filtered_records = [r for r in all_records if r["type"] == "TXT"]
|
|
85
|
-
|
|
84
|
+
|
|
86
85
|
return filtered_records
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: certapi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Python Package for managing keys, request SSL certificates from ACME.
|
|
5
5
|
Home-page: https://github.com/mesudip/certapi
|
|
6
6
|
Author: Sudip Bhattarai
|
|
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
# CertApi
|
|
15
15
|
|
|
16
16
|
CertApi is a Python package for requesting SSL certificates from ACME.
|
|
17
|
-
This is
|
|
17
|
+
This is to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
|
|
18
18
|
|
|
19
19
|
> ⚠️ Warning: This project is not polished for production use. Please stay tuned for the LTS `v1.0.0` release.
|
|
20
20
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|