certapi 0.1.0__py3-none-any.whl

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/db.py ADDED
@@ -0,0 +1,365 @@
1
+ import os
2
+ import sqlite3
3
+ from typing import Union, Tuple, Optional
4
+
5
+ from .crypto import *
6
+ from flask import g
7
+ from abc import ABC, abstractmethod
8
+
9
+ from .crypto_classes import Key
10
+
11
+
12
+ class KeyStore(ABC):
13
+ account_key: RSAPrivateKey
14
+
15
+ @abstractmethod
16
+ def save_key(self, key: RSAPrivateKey, name: str = None) -> int | str:
17
+ pass
18
+
19
+ @abstractmethod
20
+ def gen_key(self, name: str = None, size: int = 4096) -> RSAPrivateKey:
21
+ pass
22
+
23
+ @abstractmethod
24
+ def save_cert(self, private_key_id: int, cert: Certificate, domains: List[str], name: str = None) -> int:
25
+ pass
26
+
27
+ @abstractmethod
28
+ def get_cert(self, domain: str) -> None | Tuple[int | str, Key, Certificate]:
29
+ pass
30
+
31
+
32
+ class SqliteKeyStore(KeyStore):
33
+ def __init__(self, db_path="db/database.db"):
34
+ self.db_path = db_path
35
+ self._initialize_db()
36
+ self.account_key = self._init_account_key()
37
+
38
+ def _initialize_db(self):
39
+ os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
40
+ with sqlite3.connect(self.db_path) as conn:
41
+ conn.executescript(
42
+ """
43
+ CREATE TABLE IF NOT EXISTS private_keys (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ name VARCHAR(50) NULL,
46
+ content BLOB
47
+ );
48
+ CREATE TABLE IF NOT EXISTS certificates (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ name VARCHAR(50) NULL,
51
+ priv_id INTEGER REFERENCES private_keys NOT NULL,
52
+ content BLOB,
53
+ sign_id INTEGER REFERENCES private_keys NULL
54
+ );
55
+ CREATE TABLE IF NOT EXISTS ssl_domains (
56
+ domain VARCHAR(255),
57
+ certificate_id INTEGER REFERENCES certificates
58
+ );
59
+ CREATE TABLE IF NOT EXISTS ssl_wildcards (
60
+ domain VARCHAR(255),
61
+ certificate_id INTEGER REFERENCES certificates
62
+ );
63
+ """
64
+ )
65
+
66
+ def _get_db_connection(self):
67
+ if "db" not in g:
68
+ g.db = sqlite3.connect(self.db_path)
69
+ return g.db
70
+
71
+ def save_key(self, key: RSAPrivateKey, name: str = None) -> int:
72
+ conn = self._get_db_connection()
73
+ cur = conn.cursor()
74
+ cur.execute("INSERT INTO private_keys (name, content) VALUES (?, ?)", (name, key_to_der(key)))
75
+ cur.close()
76
+ conn.commit()
77
+ return cur.lastrowid
78
+
79
+ def gen_key(self, name: str = None, size: int = 4096) -> RSAPrivateKey:
80
+ key = gen_key_rsa(size)
81
+ self.save_key(key, name)
82
+ return key
83
+
84
+ def save_cert(self, private_key_id: int, cert: Certificate, domains: List[str], name: str = None) -> int:
85
+ conn = self._get_db_connection()
86
+ cur = conn.cursor()
87
+ cur.execute(
88
+ "INSERT INTO certificates (name, priv_id, content) VALUES (?, ?, ?)",
89
+ (name, private_key_id, cert.public_bytes(serialization.Encoding.DER)),
90
+ )
91
+ cert_id = cur.lastrowid
92
+
93
+ for domain in domains:
94
+ cur.execute("INSERT INTO ssl_domains (domain, certificate_id) VALUES (?, ?)", (domain, cert_id))
95
+ cur.close()
96
+ conn.commit()
97
+ return cert_id
98
+
99
+ def get_cert(self, domain: str) -> None | Tuple[int | str, Key, Certificate]:
100
+ conn = self._get_db_connection()
101
+ cur = conn.cursor()
102
+ cur.execute(
103
+ """
104
+ SELECT c.id, p.content, c.content
105
+ FROM ssl_domains s
106
+ JOIN certificates c ON s.certificate_id = c.id
107
+ JOIN private_keys p ON c.priv_id = p.id
108
+ WHERE s.domain = ?
109
+ """,
110
+ (domain,),
111
+ )
112
+ res = cur.fetchone()
113
+
114
+ cur.close()
115
+
116
+ return res if res is None else (res[0], Key.from_der(res[1]), cert_from_der(res[2]))
117
+
118
+ def _init_account_key(self) -> RSAPrivateKey:
119
+ acme_key_name = "ACME Account Key"
120
+ conn = sqlite3.connect(self.db_path)
121
+ account_key_data = conn.execute("SELECT content FROM private_keys WHERE name = ?", [acme_key_name]).fetchone()
122
+
123
+ if not account_key_data:
124
+ account_key = self.gen_key(acme_key_name)
125
+ else:
126
+ account_key = key_from_der(account_key_data[0])
127
+
128
+ print(key_to_pem(account_key).decode("utf-8"))
129
+ conn.close()
130
+ return account_key
131
+
132
+
133
+ class FilesystemKeyStore(KeyStore):
134
+ def __init__(self, base_dir="."):
135
+ self.keys_dir = os.path.join(base_dir, "keys")
136
+ self.certs_dir = os.path.join(base_dir, "certs")
137
+ os.makedirs(self.keys_dir, exist_ok=True)
138
+ os.makedirs(self.certs_dir, exist_ok=True)
139
+ self._init_account_key()
140
+
141
+ def _init_account_key(self) -> RSAPrivateKey:
142
+ acme_key_name = "acme_account"
143
+ self.account_key = self.find_key(acme_key_name)
144
+ if self.account_key is None:
145
+ self.account_key = self.gen_key("acme_account")
146
+ return self.account_key
147
+
148
+ def save_key(self, key: RSAPrivateKey, name: str = None) -> str:
149
+ key_path = os.path.join(self.keys_dir, f"{name}.key")
150
+ with open(key_path, "wb") as f:
151
+ f.write(key_to_pem(key))
152
+ return name # Dummy ID since filesystem does not use numeric IDs
153
+
154
+ def gen_key(self, name: str = None, size: int = 4096) -> RSAPrivateKey:
155
+ key = gen_key_rsa(size)
156
+ self.save_key(key, name)
157
+ return key
158
+
159
+ def find_key(self, name: str) -> Union[None, RSAPrivateKey]:
160
+ key_path = os.path.join(self.keys_dir, f"{name}.key")
161
+ if os.path.exists(key_path):
162
+ with open(key_path, "rb") as f:
163
+ key_data = f.read()
164
+ return key_from_pem(key_data)
165
+ return None
166
+
167
+ def find_cert(self, name: str) -> Union[None, Certificate]:
168
+ cert_path = os.path.join(self.certs_dir, f"{name}.crt")
169
+ if os.path.exists(cert_path):
170
+ with open(cert_path, "rb") as f:
171
+ cert_data = f.read()
172
+ return cert_from_pem(cert_data)
173
+ return None
174
+
175
+ def save_cert(self, private_key_id: str, cert: Certificate, domains: list, name: str = None) -> int:
176
+ if name:
177
+ cert_path = os.path.join(self.certs_dir, f"{name}.crt")
178
+ with open(cert_path, "wb") as f:
179
+ f.write(cert_to_pem(cert))
180
+ key_content = None
181
+ key_path = os.path.join(self.keys_dir, f"{private_key_id}.key")
182
+ with open(key_path, "rb") as f:
183
+ key_content = f.read()
184
+ for domain in domains:
185
+ if domain != private_key_id:
186
+ with open(os.path.join(self.keys_dir, f"{domain}.key"), "wb") as f:
187
+ f.write(key_content)
188
+ domain_cert_path = os.path.join(self.certs_dir, f"{domain}.crt")
189
+ with open(domain_cert_path, "wb") as f:
190
+ f.write(cert_to_pem(cert))
191
+
192
+ return name if name else domains[0] # Dummy ID since filesystem does not use numeric IDs
193
+
194
+ def get_cert(self, domain: str) -> None | Tuple[str, Key, Certificate]:
195
+ cert_path = os.path.join(self.certs_dir, f"{domain}.crt")
196
+ key_path = os.path.join(self.keys_dir, f"{domain}.key")
197
+ key = None
198
+ cert = None
199
+ if os.path.exists(key_path):
200
+ try:
201
+ with open(key_path, "rb") as f:
202
+ key = Key.from_pem(f.read())
203
+ except ValueError:
204
+ pass
205
+
206
+ if os.path.exists(cert_path):
207
+ try:
208
+ with open(cert_path, "rb") as f:
209
+ cert = cert_from_pem(f.read())
210
+ except ValueError:
211
+ pass
212
+
213
+ if cert is None or key is None:
214
+ return None
215
+ return (domain, key, cert)
216
+
217
+
218
+ class PostgresKeyStore(KeyStore):
219
+ def __init__(self, db_url="postgresql://user:password@localhost/dbname"):
220
+ self.db_url = db_url
221
+
222
+ import psycopg2
223
+
224
+ self.psycopg2 = psycopg2
225
+
226
+ def setup(self):
227
+ self._initialize_pool()
228
+ self._initialize_db()
229
+ self.account_key = self._init_account_key()
230
+
231
+ def _initialize_pool(self):
232
+ """Initialize the connection pool."""
233
+ from psycopg2.pool import SimpleConnectionPool
234
+
235
+ self.pool = SimpleConnectionPool(1, 1, self.db_url)
236
+
237
+ def _check_connection(self, conn):
238
+ """Check if the connection is alive by using the `ping` method."""
239
+ try:
240
+ conn.ping() # This will raise an exception if the connection is not alive.
241
+ return True
242
+ except self.psycopg2.OperationalError:
243
+ return False
244
+
245
+ def _get_db_connection(self):
246
+ """Get a connection from the pool and ensure it is healthy."""
247
+ conn = self.pool.getconn()
248
+
249
+ # Check connection health
250
+ if not self._check_connection(conn):
251
+ print("Connection is not healthy, reconnecting...")
252
+ self.pool.putconn(conn, close=True) # Close the bad connection
253
+ conn = self.pool.getconn() # Get a fresh connection
254
+
255
+ return conn
256
+
257
+ def _initialize_db(self):
258
+ """Initializes the database with necessary tables."""
259
+ conn = self._get_db_connection()
260
+ cur = conn.cursor()
261
+
262
+ cur.execute(
263
+ """
264
+ CREATE TABLE IF NOT EXISTS private_keys (
265
+ id SERIAL PRIMARY KEY,
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()
289
+
290
+ def save_key(self, key: RSAPrivateKey, name: str = None) -> int:
291
+ """Saves a private key in the database."""
292
+ conn = self._get_db_connection()
293
+ cur = conn.cursor()
294
+ cur.execute("INSERT INTO private_keys (name, content) VALUES (%s, %s) RETURNING id", (name, key_to_der(key)))
295
+ key_id = cur.fetchone()[0]
296
+ cur.close()
297
+ conn.commit()
298
+ return key_id
299
+
300
+ def gen_key(self, name: str = None, size: int = 4096) -> RSAPrivateKey:
301
+ """Generates a new RSA private key and saves it."""
302
+ key = gen_key_rsa(size)
303
+ self.save_key(key, name)
304
+ return key
305
+
306
+ def save_cert(self, private_key_id: int, cert: Certificate, domains: List[str], name: str = None) -> int:
307
+ """Saves a certificate along with associated domains."""
308
+ conn = self._get_db_connection()
309
+ cur = conn.cursor()
310
+
311
+ # Insert certificate
312
+ cur.execute(
313
+ "INSERT INTO certificates (name, priv_id, content) VALUES (%s, %s, %s) RETURNING id",
314
+ (name, private_key_id, cert.public_bytes(serialization.Encoding.DER)),
315
+ )
316
+ cert_id = cur.fetchone()[0]
317
+
318
+ # Insert associated domains
319
+ for domain in domains:
320
+ cur.execute("INSERT INTO ssl_domains (domain, certificate_id) VALUES (%s, %s)", (domain, cert_id))
321
+
322
+ cur.close()
323
+ conn.commit()
324
+ return cert_id
325
+
326
+ def get_cert(self, domain: str) -> Optional[Tuple[int, RSAPrivateKey, Certificate]]:
327
+ """Fetches a certificate and its associated private key for a domain."""
328
+ conn = self._get_db_connection()
329
+ cur = conn.cursor()
330
+
331
+ cur.execute(
332
+ """
333
+ SELECT c.id, p.content, c.content
334
+ FROM ssl_domains s
335
+ JOIN certificates c ON s.certificate_id = c.id
336
+ JOIN private_keys p ON c.priv_id = p.id
337
+ WHERE s.domain = %s
338
+ """,
339
+ (domain,),
340
+ )
341
+ res = cur.fetchone()
342
+
343
+ cur.close()
344
+ conn.commit()
345
+
346
+ if res:
347
+ return (res[0], Key.from_der(res[1]), cert_from_der(res[2]))
348
+ return None
349
+
350
+ def _init_account_key(self) -> RSAPrivateKey:
351
+ """Initializes or retrieves the ACME account key."""
352
+ acme_key_name = "ACME Account Key"
353
+ conn = self._get_db_connection()
354
+ cur = conn.cursor()
355
+ cur.execute("SELECT content FROM private_keys WHERE name = %s", (acme_key_name,))
356
+ account_key_data = cur.fetchone()
357
+
358
+ if not account_key_data:
359
+ account_key = self.gen_key(acme_key_name)
360
+ else:
361
+ account_key = key_from_der(account_key_data[0])
362
+
363
+ cur.close()
364
+
365
+ return account_key
certapi/util.py ADDED
@@ -0,0 +1,19 @@
1
+ import json
2
+ import base64
3
+ from typing import Union
4
+
5
+
6
+ def b64_encode(data: Union[str, bytes, bytearray, dict, list]) -> bytes:
7
+ if type(data) in (dict, list):
8
+ data = json.dumps(data).encode("utf-8")
9
+ elif type(data) == str:
10
+ data = data.encode("utf-8")
11
+ return base64.urlsafe_b64encode(data).rstrip(b"=")
12
+
13
+
14
+ def b64_string(data: Union[str, bytes, bytearray, dict, list]) -> str:
15
+ return b64_encode(data).decode("utf-8")
16
+
17
+
18
+ def crypto():
19
+ return None
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.1
2
+ Name: certapi
3
+ Version: 0.1.0
4
+ Summary: Python Package for managing keys, request SSL certificates from ACME.
5
+ Home-page: https://github.com/mesudip/certmanager
6
+ Author: Sudip Bhattarai
7
+ Author-email: sudipbhattarai100@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: cryptography
14
+ Requires-Dist: requests
15
+
16
+ # CertManager
17
+
18
+ CertManager is a Python package for requesting SSL certificates from ACME.
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
+
21
+ ## Installation
22
+
23
+ You can install CertManager using pip:
24
+
25
+ ```bash
26
+ pip install certapi
27
+ ```
28
+
29
+ ## Example Usage
30
+
31
+ ```python
32
+ import json
33
+ from certmanager import FileSystemChallengeStore, FilesystemKeyStore, CertAuthority
34
+
35
+ key_store = FilesystemKeyStore("data")
36
+ challenge_store = FileSystemChallengeStore("./acme-challenges") # this should be where your web server hosts the .well-known/acme-challenges.
37
+
38
+ certAuthority = CertAuthority(challenge_store, key_store)
39
+ certAuthority.setup()
40
+
41
+ (response,_) = certAuthority.obtainCert("example.com")
42
+
43
+ json.dumps(response.__json__(),indent=2)
44
+
45
+ ```
@@ -0,0 +1,13 @@
1
+ certapi/Acme.py,sha256=Ndcn2kDEXcSVQktisGcScnMJNMLRcv2H1aZQHHZar_E,15952
2
+ certapi/__init__.py,sha256=AW8jV-jOuqt5xW1dW8e0Am_uasVLd6Jsp0zhvho5mPA,314
3
+ certapi/certauthority.py,sha256=-GKMNyzgCjeVs4AM-aKqe5XTSMi-wP-nj3WGhZ23-pI,5759
4
+ certapi/challenge.py,sha256=OT_0zx526U0uNNIxT6Iz8wEBXJXtkSEGon_yk04GT8g,3681
5
+ certapi/crypto.py,sha256=OBdQkwqDjEt_Ths3Hlqou0D4tSZHBr_m3F7cQJ-RzZY,7481
6
+ certapi/crypto_classes.py,sha256=VBIBAFRl4PO_e2K6xperD6kZsQh9LX0y1nPESrXVlYM,4028
7
+ certapi/custom_certauthority.py,sha256=YAoqBIVKcSywbXUhW6d0HwxjKk7fA0VMxr9XRO3xQi0,3521
8
+ certapi/db.py,sha256=E8tAZ4Fqt6UD4a4v0uQaVsLZR3Gf22fpcnTPVDgONvM,12821
9
+ certapi/util.py,sha256=M1EUcaOFXqHGrq54_AXgI9hqZpneRBqY7V-fyDUPUIM,477
10
+ certapi-0.1.0.dist-info/METADATA,sha256=vFr1gPhbwhRgWHNewOQvCA633OVw_YgKDDmZzUsBs80,1309
11
+ certapi-0.1.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
12
+ certapi-0.1.0.dist-info/top_level.txt,sha256=zK_AUsm1tjAi3d6hpO6rhh5BdfpSj8__9ROReFanBxo,8
13
+ certapi-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ certapi