certapi 0.1.0__tar.gz → 0.1.1__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.1.0 → certapi-0.1.1}/PKG-INFO +6 -6
- {certapi-0.1.0 → certapi-0.1.1}/README.md +4 -4
- {certapi-0.1.0 → certapi-0.1.1}/setup.py +2 -2
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi/Acme.py +52 -32
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi/certauthority.py +6 -9
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi/db.py +97 -95
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi.egg-info/PKG-INFO +6 -6
- {certapi-0.1.0 → certapi-0.1.1}/tests/test_custom_certauthority.py +2 -2
- {certapi-0.1.0 → certapi-0.1.1}/MANIFEST.in +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/pyproject.toml +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/setup.cfg +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi/__init__.py +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi/challenge.py +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi/crypto.py +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi/crypto_classes.py +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi/custom_certauthority.py +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi/util.py +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi.egg-info/SOURCES.txt +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi.egg-info/requires.txt +0 -0
- {certapi-0.1.0 → certapi-0.1.1}/src/certapi.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: certapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Python Package for managing keys, request SSL certificates from ACME.
|
|
5
|
-
Home-page: https://github.com/mesudip/
|
|
5
|
+
Home-page: https://github.com/mesudip/certapi
|
|
6
6
|
Author: Sudip Bhattarai
|
|
7
7
|
Author-email: sudipbhattarai100@gmail.com
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -13,14 +13,14 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
Requires-Dist: cryptography
|
|
14
14
|
Requires-Dist: requests
|
|
15
15
|
|
|
16
|
-
#
|
|
16
|
+
# CertApi
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
CertApi is a Python package for requesting SSL certificates from ACME.
|
|
19
19
|
This is supposed to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
-
You can install
|
|
23
|
+
You can install CertApi using pip:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
pip install certapi
|
|
@@ -30,7 +30,7 @@ pip install certapi
|
|
|
30
30
|
|
|
31
31
|
```python
|
|
32
32
|
import json
|
|
33
|
-
from
|
|
33
|
+
from certapi import FileSystemChallengeStore, FilesystemKeyStore, CertAuthority
|
|
34
34
|
|
|
35
35
|
key_store = FilesystemKeyStore("data")
|
|
36
36
|
challenge_store = FileSystemChallengeStore("./acme-challenges") # this should be where your web server hosts the .well-known/acme-challenges.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CertApi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CertApi is a Python package for requesting SSL certificates from ACME.
|
|
4
4
|
This is supposed to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
|
|
5
5
|
|
|
6
6
|
## Installation
|
|
7
7
|
|
|
8
|
-
You can install
|
|
8
|
+
You can install CertApi using pip:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
pip install certapi
|
|
@@ -15,7 +15,7 @@ pip install certapi
|
|
|
15
15
|
|
|
16
16
|
```python
|
|
17
17
|
import json
|
|
18
|
-
from
|
|
18
|
+
from certapi import FileSystemChallengeStore, FilesystemKeyStore, CertAuthority
|
|
19
19
|
|
|
20
20
|
key_store = FilesystemKeyStore("data")
|
|
21
21
|
challenge_store = FileSystemChallengeStore("./acme-challenges") # this should be where your web server hosts the .well-known/acme-challenges.
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="certapi",
|
|
5
|
-
version="0.1.
|
|
5
|
+
version="0.1.1",
|
|
6
6
|
packages=find_packages(where="src"),
|
|
7
7
|
package_dir={"": "src"},
|
|
8
8
|
install_requires=[
|
|
@@ -15,7 +15,7 @@ setup(
|
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
16
16
|
author="Sudip Bhattarai",
|
|
17
17
|
author_email="sudipbhattarai100@gmail.com",
|
|
18
|
-
url="https://github.com/mesudip/
|
|
18
|
+
url="https://github.com/mesudip/certapi",
|
|
19
19
|
classifiers=[
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
21
21
|
"License :: OSI Approved :: MIT License",
|
|
@@ -12,15 +12,16 @@ from .crypto import sign, digest_sha256, csr_to_der, jwk, get_algorithm_name
|
|
|
12
12
|
from .util import b64_encode, b64_string
|
|
13
13
|
|
|
14
14
|
# acme_url = os.environ.get("LETSENCRYPT_API", "https://acme-staging-v02.api.letsencrypt.org/directory")
|
|
15
|
-
acme_url = os.environ.get("LETSENCRYPT_API",
|
|
15
|
+
acme_url = os.environ.get("LETSENCRYPT_API", None)
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class AcmeError(Exception):
|
|
19
19
|
def __init__(self, message, detail, step):
|
|
20
|
-
super().__init__(step,message)
|
|
20
|
+
super().__init__(step, message)
|
|
21
21
|
self.message: str = message
|
|
22
22
|
self.step: str = step
|
|
23
23
|
self.detail: dict = detail
|
|
24
|
+
self.can_retry = False
|
|
24
25
|
|
|
25
26
|
def json_obj(self) -> dict:
|
|
26
27
|
return {"message": self.message, "step": self.step, "detail": self.detail}
|
|
@@ -36,6 +37,7 @@ class AcmeNetworkError(AcmeError, requests.RequestException):
|
|
|
36
37
|
requests.RequestException.__init__(self, request=request)
|
|
37
38
|
AcmeError.__init__(self, message, detail, step)
|
|
38
39
|
requests.RequestException.__init__(self, request=request) # Pass message to RequestException
|
|
40
|
+
self.can_retry = True
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
class AcmeHttpError(AcmeError, requests.HTTPError):
|
|
@@ -45,7 +47,7 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
|
|
|
45
47
|
|
|
46
48
|
def __init__(self, response: requests.Response, step: str):
|
|
47
49
|
requests.HTTPError.__init__(self, response=response)
|
|
48
|
-
self.response=response
|
|
50
|
+
self.response = response
|
|
49
51
|
(message, detail) = self.extract_acme_response_error()
|
|
50
52
|
AcmeError.__init__(self, message, detail, step)
|
|
51
53
|
|
|
@@ -121,8 +123,8 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
|
|
|
121
123
|
message = err_detail
|
|
122
124
|
|
|
123
125
|
if message is None:
|
|
124
|
-
if res_json.get(
|
|
125
|
-
message = res_json[
|
|
126
|
+
if res_json.get("detail"):
|
|
127
|
+
message = res_json["detail"]
|
|
126
128
|
else:
|
|
127
129
|
message = "Received status=" + str(self.response.status_code) + " from AMCE server"
|
|
128
130
|
if error is None:
|
|
@@ -141,22 +143,28 @@ class AcmeInvaliOrderError(AcmeHttpError):
|
|
|
141
143
|
super().__init__(response, step)
|
|
142
144
|
|
|
143
145
|
|
|
146
|
+
class AcmeInvaliNonceError(AcmeHttpError):
|
|
147
|
+
def __init__(self, response: requests.Response, step: str):
|
|
148
|
+
super().__init__(response, step)
|
|
149
|
+
self.can_retry = True
|
|
150
|
+
|
|
151
|
+
|
|
144
152
|
def request(method, step: str, url: str, json=None, headers=None, throw=True) -> requests.Response:
|
|
145
|
-
res=None
|
|
153
|
+
res = None
|
|
146
154
|
try:
|
|
147
155
|
res = requests.request(method, url, json=json, headers=headers, timeout=15)
|
|
148
|
-
print("Request ["+str(res.status_code)+"] : " + method + " " + url + " step=" + step)
|
|
156
|
+
print("Request [" + str(res.status_code) + "] : " + method + " " + url + " step=" + step)
|
|
149
157
|
except requests.HTTPError as e:
|
|
150
158
|
status = res.status_code if res else None
|
|
151
159
|
status = status if status else (e.response.status_code if e.response else None)
|
|
152
160
|
if status:
|
|
153
|
-
print("Request ["+str(status)+"] : " + method + " " + url + " step=" + step)
|
|
161
|
+
print("Request [" + str(status) + "] : " + method + " " + url + " step=" + step)
|
|
154
162
|
else:
|
|
155
163
|
print("Request : " + method + " " + url + " step=" + step)
|
|
156
164
|
|
|
157
165
|
raise e
|
|
158
166
|
except requests.RequestException as e:
|
|
159
|
-
print("Request : " + method + " " + url + " step=" + step)
|
|
167
|
+
print("Request : " + str(method) + " " + str(url) + " step=" + str(step))
|
|
160
168
|
raise AcmeNetworkError(
|
|
161
169
|
e.request,
|
|
162
170
|
f"Error communicating with ACME server",
|
|
@@ -169,6 +177,18 @@ def request(method, step: str, url: str, json=None, headers=None, throw=True) ->
|
|
|
169
177
|
step,
|
|
170
178
|
)
|
|
171
179
|
if 199 <= res.status_code > 299:
|
|
180
|
+
[print(x, y) for (x, y) in res.headers.items()]
|
|
181
|
+
print("Response:", res.text)
|
|
182
|
+
json_data = None
|
|
183
|
+
try:
|
|
184
|
+
json_data = res.json()
|
|
185
|
+
except requests.RequestException as e:
|
|
186
|
+
pass
|
|
187
|
+
if json_data and json_data.get("type"):
|
|
188
|
+
errorType = json_data["type"]
|
|
189
|
+
if errorType == "urn:ietf:params:acme:error:badNonce":
|
|
190
|
+
raise AcmeInvaliNonceError(res, step=step)
|
|
191
|
+
|
|
172
192
|
if throw:
|
|
173
193
|
raise AcmeHttpError(res, step=step)
|
|
174
194
|
return res
|
|
@@ -192,7 +212,7 @@ class Acme:
|
|
|
192
212
|
# json web key format for public key
|
|
193
213
|
self.jwk = jwk(self.account_key)
|
|
194
214
|
self.nonce = []
|
|
195
|
-
self.acme_url = url
|
|
215
|
+
self.acme_url = url if url else self.URL_PROD
|
|
196
216
|
self.key_id = None
|
|
197
217
|
self.directory = None
|
|
198
218
|
self._nonce_lock = threading.Lock() # Mutex for safe access to nonce
|
|
@@ -210,19 +230,12 @@ class Acme:
|
|
|
210
230
|
url = self._directory(path_name)
|
|
211
231
|
return self._signed_req(url, payload, depth, step="Acme request:" + path_name)
|
|
212
232
|
|
|
213
|
-
def
|
|
214
|
-
self, url, payload: Union[str, dict, list, bytes, None] = None, depth=0, step="Acme Request", throw=True
|
|
215
|
-
) -> requests.Response:
|
|
216
|
-
payload64 = b64_encode(payload) if payload is not None else b""
|
|
233
|
+
def get_nonce(self, step: str, counter=1):
|
|
217
234
|
nonce = None
|
|
218
|
-
with self._nonce_lock: # Acquire
|
|
219
|
-
# Check if there are any nonces available
|
|
235
|
+
with self._nonce_lock: # Acquire nonce
|
|
220
236
|
if self.nonce:
|
|
221
|
-
# Pop the first nonce from the list
|
|
222
237
|
nonce = self.nonce.pop(0)
|
|
223
|
-
|
|
224
|
-
# Fetch a new nonce if the list is empty
|
|
225
|
-
nonce = (
|
|
238
|
+
return (
|
|
226
239
|
nonce
|
|
227
240
|
if nonce
|
|
228
241
|
else get(
|
|
@@ -230,10 +243,20 @@ class Acme:
|
|
|
230
243
|
).headers.get("Replay-Nonce")
|
|
231
244
|
)
|
|
232
245
|
|
|
246
|
+
def record_nonce(self, response: requests.Response) -> requests.Response:
|
|
247
|
+
with self._nonce_lock:
|
|
248
|
+
self.nonce.append(response.headers.get("Replay-Nonce", None))
|
|
249
|
+
return response
|
|
250
|
+
|
|
251
|
+
def _signed_req(
|
|
252
|
+
self, url, payload: Union[str, dict, list, bytes, None] = None, depth=0, step="Acme Request", throw=True
|
|
253
|
+
) -> requests.Response:
|
|
254
|
+
payload64 = b64_encode(payload) if payload is not None else b""
|
|
255
|
+
|
|
233
256
|
protected = {
|
|
234
257
|
"url": url,
|
|
235
258
|
"alg": get_algorithm_name(self.account_key),
|
|
236
|
-
"nonce":
|
|
259
|
+
"nonce": self.get_nonce(step),
|
|
237
260
|
}
|
|
238
261
|
|
|
239
262
|
if self.key_id:
|
|
@@ -246,19 +269,16 @@ class Acme:
|
|
|
246
269
|
"payload": payload64.decode("utf-8"),
|
|
247
270
|
"signature": b64_string(sign(self.account_key, b".".join([protectedb64, payload64]))),
|
|
248
271
|
}
|
|
272
|
+
try:
|
|
249
273
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
print(response.text)
|
|
257
|
-
print("-" * 60)
|
|
274
|
+
response = post(step, url, json=payload, headers={"Content-Type": "application/jose+json"}, throw=throw)
|
|
275
|
+
except AcmeError as e:
|
|
276
|
+
if e.can_retry and depth <= 0:
|
|
277
|
+
return self._signed_req(url, payload, depth + 1, step, throw)
|
|
278
|
+
else:
|
|
279
|
+
raise e
|
|
258
280
|
|
|
259
|
-
|
|
260
|
-
self.nonce.append(response.headers.get("Replay-Nonce", None))
|
|
261
|
-
return response
|
|
281
|
+
return self.record_nonce(response)
|
|
262
282
|
|
|
263
283
|
def register(self):
|
|
264
284
|
response = self._directory_req("newAccount", {"termsOfServiceAgreed": True})
|
|
@@ -15,8 +15,8 @@ from .db import KeyStore
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class CertAuthority:
|
|
18
|
-
def __init__(self, challenge_store: challenge.ChallengeStore, key_store: KeyStore,acme_url=None):
|
|
19
|
-
self.acme = Acme(key_store.account_key,url=acme_url)
|
|
18
|
+
def __init__(self, challenge_store: challenge.ChallengeStore, key_store: KeyStore, acme_url=None):
|
|
19
|
+
self.acme = Acme(key_store.account_key, url=acme_url)
|
|
20
20
|
self.key_store = key_store
|
|
21
21
|
self.challengesStore: challenge.ChallengeStore = challenge_store
|
|
22
22
|
|
|
@@ -28,9 +28,7 @@ class CertAuthority:
|
|
|
28
28
|
elif res.status_code != 200:
|
|
29
29
|
raise Exception("Acme registration didn't return 200 or 201 ", res.json())
|
|
30
30
|
|
|
31
|
-
def obtainCert(
|
|
32
|
-
self, host: Union[str, List[str]]
|
|
33
|
-
) -> Union[Tuple["CertificateResponse", None], Tuple[None, requests.Response]]:
|
|
31
|
+
def obtainCert(self, host: Union[str, List[str]]) -> "CertificateResponse":
|
|
34
32
|
if type(host) == str:
|
|
35
33
|
host = [host]
|
|
36
34
|
|
|
@@ -73,12 +71,11 @@ class CertAuthority:
|
|
|
73
71
|
order.refresh() # is this refresh necessary?
|
|
74
72
|
|
|
75
73
|
if order.status == "valid":
|
|
76
|
-
|
|
74
|
+
certificate = order.get_certificate()
|
|
77
75
|
key_id = self.key_store.save_key(private_key, missing[0])
|
|
78
76
|
cert_id = self.key_store.save_cert(key_id, certificate, missing)
|
|
79
77
|
issued_cert = IssuedCert(key_to_pem(private_key), certificate, missing)
|
|
80
|
-
|
|
81
|
-
return (response, None)
|
|
78
|
+
return createExistingResponse(existing, [issued_cert])
|
|
82
79
|
elif order.status == "processing":
|
|
83
80
|
if count == 0:
|
|
84
81
|
return None
|
|
@@ -87,7 +84,7 @@ class CertAuthority:
|
|
|
87
84
|
|
|
88
85
|
return obtain_cert()
|
|
89
86
|
else:
|
|
90
|
-
return createExistingResponse(existing, [])
|
|
87
|
+
return createExistingResponse(existing, [])
|
|
91
88
|
|
|
92
89
|
|
|
93
90
|
def createExistingResponse(existing: Dict[str, Tuple[int | str, Key, Certificate]], issued_certs: List["IssuedCert"]):
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sqlite3
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Tuple, Optional
|
|
4
|
+
from contextlib import contextmanager
|
|
4
5
|
|
|
5
6
|
from .crypto import *
|
|
6
|
-
from flask import g
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
|
|
9
9
|
from .crypto_classes import Key
|
|
@@ -32,6 +32,7 @@ class KeyStore(ABC):
|
|
|
32
32
|
class SqliteKeyStore(KeyStore):
|
|
33
33
|
def __init__(self, db_path="db/database.db"):
|
|
34
34
|
self.db_path = db_path
|
|
35
|
+
self.db = None
|
|
35
36
|
self._initialize_db()
|
|
36
37
|
self.account_key = self._init_account_key()
|
|
37
38
|
|
|
@@ -64,9 +65,18 @@ class SqliteKeyStore(KeyStore):
|
|
|
64
65
|
)
|
|
65
66
|
|
|
66
67
|
def _get_db_connection(self):
|
|
67
|
-
if
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if self.db:
|
|
69
|
+
return self.db
|
|
70
|
+
target = self
|
|
71
|
+
try:
|
|
72
|
+
from flask import g
|
|
73
|
+
|
|
74
|
+
target = g
|
|
75
|
+
except:
|
|
76
|
+
pass
|
|
77
|
+
if "db" not in target or target.db is None:
|
|
78
|
+
target.db = sqlite3.connect(self.db_path)
|
|
79
|
+
return target.db
|
|
70
80
|
|
|
71
81
|
def save_key(self, key: RSAPrivateKey, name: str = None) -> int:
|
|
72
82
|
conn = self._get_db_connection()
|
|
@@ -218,7 +228,6 @@ class FilesystemKeyStore(KeyStore):
|
|
|
218
228
|
class PostgresKeyStore(KeyStore):
|
|
219
229
|
def __init__(self, db_url="postgresql://user:password@localhost/dbname"):
|
|
220
230
|
self.db_url = db_url
|
|
221
|
-
|
|
222
231
|
import psycopg2
|
|
223
232
|
|
|
224
233
|
self.psycopg2 = psycopg2
|
|
@@ -232,7 +241,7 @@ class PostgresKeyStore(KeyStore):
|
|
|
232
241
|
"""Initialize the connection pool."""
|
|
233
242
|
from psycopg2.pool import SimpleConnectionPool
|
|
234
243
|
|
|
235
|
-
self.pool = SimpleConnectionPool(1,
|
|
244
|
+
self.pool = SimpleConnectionPool(1, 10, self.db_url)
|
|
236
245
|
|
|
237
246
|
def _check_connection(self, conn):
|
|
238
247
|
"""Check if the connection is alive by using the `ping` method."""
|
|
@@ -242,59 +251,62 @@ class PostgresKeyStore(KeyStore):
|
|
|
242
251
|
except self.psycopg2.OperationalError:
|
|
243
252
|
return False
|
|
244
253
|
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
@contextmanager
|
|
255
|
+
def get_connection(self):
|
|
256
|
+
"""Context manager for acquiring and releasing a database connection."""
|
|
247
257
|
conn = self.pool.getconn()
|
|
248
258
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
try:
|
|
260
|
+
# Check connection health
|
|
261
|
+
if not self._check_connection(conn):
|
|
262
|
+
print("Connection is not healthy, reconnecting...")
|
|
263
|
+
self.pool.putconn(conn, close=True) # Close the bad connection
|
|
264
|
+
conn = self.pool.getconn() # Get a fresh connection
|
|
265
|
+
|
|
266
|
+
# Yield the connection to the caller
|
|
267
|
+
yield conn
|
|
268
|
+
finally:
|
|
269
|
+
# Return the connection to the pool
|
|
270
|
+
self.pool.putconn(conn)
|
|
256
271
|
|
|
257
272
|
def _initialize_db(self):
|
|
258
273
|
"""Initializes the database with necessary tables."""
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
274
|
+
with self.get_connection() as conn:
|
|
275
|
+
# Use the connection directly to execute the query
|
|
276
|
+
conn.execute(
|
|
277
|
+
"""
|
|
278
|
+
CREATE TABLE IF NOT EXISTS private_keys (
|
|
279
|
+
id SERIAL PRIMARY KEY,
|
|
280
|
+
name VARCHAR(50) NULL,
|
|
281
|
+
content BYTEA
|
|
282
|
+
);
|
|
283
|
+
CREATE TABLE IF NOT EXISTS certificates (
|
|
284
|
+
id SERIAL PRIMARY KEY,
|
|
285
|
+
name VARCHAR(50) NULL,
|
|
286
|
+
priv_id INTEGER REFERENCES private_keys NOT NULL,
|
|
287
|
+
content BYTEA,
|
|
288
|
+
sign_id INTEGER REFERENCES private_keys NULL
|
|
289
|
+
);
|
|
290
|
+
CREATE TABLE IF NOT EXISTS ssl_domains (
|
|
291
|
+
domain VARCHAR(255),
|
|
292
|
+
certificate_id INTEGER REFERENCES certificates
|
|
293
|
+
);
|
|
294
|
+
CREATE TABLE IF NOT EXISTS ssl_wildcards (
|
|
295
|
+
domain VARCHAR(255),
|
|
296
|
+
certificate_id INTEGER REFERENCES certificates
|
|
297
|
+
);
|
|
263
298
|
"""
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
name VARCHAR(50) NULL,
|
|
267
|
-
content BYTEA
|
|
268
|
-
);
|
|
269
|
-
CREATE TABLE IF NOT EXISTS certificates (
|
|
270
|
-
id SERIAL PRIMARY KEY,
|
|
271
|
-
name VARCHAR(50) NULL,
|
|
272
|
-
priv_id INTEGER REFERENCES private_keys NOT NULL,
|
|
273
|
-
content BYTEA,
|
|
274
|
-
sign_id INTEGER REFERENCES private_keys NULL
|
|
275
|
-
);
|
|
276
|
-
CREATE TABLE IF NOT EXISTS ssl_domains (
|
|
277
|
-
domain VARCHAR(255),
|
|
278
|
-
certificate_id INTEGER REFERENCES certificates
|
|
279
|
-
);
|
|
280
|
-
CREATE TABLE IF NOT EXISTS ssl_wildcards (
|
|
281
|
-
domain VARCHAR(255),
|
|
282
|
-
certificate_id INTEGER REFERENCES certificates
|
|
283
|
-
);
|
|
284
|
-
"""
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
cur.close()
|
|
288
|
-
conn.commit()
|
|
299
|
+
)
|
|
300
|
+
# No need to explicitly commit here
|
|
289
301
|
|
|
290
302
|
def save_key(self, key: RSAPrivateKey, name: str = None) -> int:
|
|
291
303
|
"""Saves a private key in the database."""
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
304
|
+
with self.get_connection() as conn:
|
|
305
|
+
# Execute the insert directly
|
|
306
|
+
conn.execute(
|
|
307
|
+
"INSERT INTO private_keys (name, content) VALUES (%s, %s) RETURNING id", (name, key_to_der(key))
|
|
308
|
+
)
|
|
309
|
+
key_id = conn.fetchone()[0] # Fetch the inserted ID
|
|
298
310
|
return key_id
|
|
299
311
|
|
|
300
312
|
def gen_key(self, name: str = None, size: int = 4096) -> RSAPrivateKey:
|
|
@@ -305,61 +317,51 @@ class PostgresKeyStore(KeyStore):
|
|
|
305
317
|
|
|
306
318
|
def save_cert(self, private_key_id: int, cert: Certificate, domains: List[str], name: str = None) -> int:
|
|
307
319
|
"""Saves a certificate along with associated domains."""
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
)
|
|
316
|
-
cert_id = cur.fetchone()[0]
|
|
320
|
+
with self.get_connection() as conn:
|
|
321
|
+
# Insert certificate and associated domains directly
|
|
322
|
+
conn.execute(
|
|
323
|
+
"INSERT INTO certificates (name, priv_id, content) VALUES (%s, %s, %s) RETURNING id",
|
|
324
|
+
(name, private_key_id, cert.public_bytes(serialization.Encoding.DER)),
|
|
325
|
+
)
|
|
326
|
+
cert_id = conn.fetchone()[0]
|
|
317
327
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
328
|
+
# Insert associated domains
|
|
329
|
+
for domain in domains:
|
|
330
|
+
conn.execute("INSERT INTO ssl_domains (domain, certificate_id) VALUES (%s, %s)", (domain, cert_id))
|
|
321
331
|
|
|
322
|
-
cur.close()
|
|
323
|
-
conn.commit()
|
|
324
332
|
return cert_id
|
|
325
333
|
|
|
326
334
|
def get_cert(self, domain: str) -> Optional[Tuple[int, RSAPrivateKey, Certificate]]:
|
|
327
335
|
"""Fetches a certificate and its associated private key for a domain."""
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
res = cur.fetchone()
|
|
342
|
-
|
|
343
|
-
cur.close()
|
|
344
|
-
conn.commit()
|
|
336
|
+
with self.get_connection() as conn:
|
|
337
|
+
# Directly execute and fetch result
|
|
338
|
+
conn.execute(
|
|
339
|
+
"""
|
|
340
|
+
SELECT c.id, p.content, c.content
|
|
341
|
+
FROM ssl_domains s
|
|
342
|
+
JOIN certificates c ON s.certificate_id = c.id
|
|
343
|
+
JOIN private_keys p ON c.priv_id = p.id
|
|
344
|
+
WHERE s.domain = %s
|
|
345
|
+
""",
|
|
346
|
+
(domain,),
|
|
347
|
+
)
|
|
348
|
+
res = conn.fetchone()
|
|
345
349
|
|
|
346
|
-
|
|
347
|
-
|
|
350
|
+
if res:
|
|
351
|
+
return (res[0], Key.from_der(res[1]), cert_from_der(res[2]))
|
|
348
352
|
return None
|
|
349
353
|
|
|
350
354
|
def _init_account_key(self) -> RSAPrivateKey:
|
|
351
355
|
"""Initializes or retrieves the ACME account key."""
|
|
352
356
|
acme_key_name = "ACME Account Key"
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
cur.close()
|
|
357
|
+
with self.get_connection() as conn:
|
|
358
|
+
# Directly execute to fetch the account key
|
|
359
|
+
conn.execute("SELECT content FROM private_keys WHERE name = %s", (acme_key_name,))
|
|
360
|
+
account_key_data = conn.fetchone()
|
|
361
|
+
|
|
362
|
+
if not account_key_data:
|
|
363
|
+
account_key = self.gen_key(acme_key_name)
|
|
364
|
+
else:
|
|
365
|
+
account_key = key_from_der(account_key_data[0])
|
|
364
366
|
|
|
365
367
|
return account_key
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: certapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Python Package for managing keys, request SSL certificates from ACME.
|
|
5
|
-
Home-page: https://github.com/mesudip/
|
|
5
|
+
Home-page: https://github.com/mesudip/certapi
|
|
6
6
|
Author: Sudip Bhattarai
|
|
7
7
|
Author-email: sudipbhattarai100@gmail.com
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -13,14 +13,14 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
Requires-Dist: cryptography
|
|
14
14
|
Requires-Dist: requests
|
|
15
15
|
|
|
16
|
-
#
|
|
16
|
+
# CertApi
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
CertApi is a Python package for requesting SSL certificates from ACME.
|
|
19
19
|
This is supposed to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
-
You can install
|
|
23
|
+
You can install CertApi using pip:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
pip install certapi
|
|
@@ -30,7 +30,7 @@ pip install certapi
|
|
|
30
30
|
|
|
31
31
|
```python
|
|
32
32
|
import json
|
|
33
|
-
from
|
|
33
|
+
from certapi import FileSystemChallengeStore, FilesystemKeyStore, CertAuthority
|
|
34
34
|
|
|
35
35
|
key_store = FilesystemKeyStore("data")
|
|
36
36
|
challenge_store = FileSystemChallengeStore("./acme-challenges") # this should be where your web server hosts the .well-known/acme-challenges.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from cryptography.hazmat.primitives import serialization
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from certapi.crypto import gen_key_rsa, key_to_pem
|
|
4
|
+
from certapi.custom_certauthority import CustomCertAuthority
|
|
5
5
|
import os
|
|
6
6
|
from contextlib import contextmanager
|
|
7
7
|
|
|
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
|