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.
Files changed (24) hide show
  1. {certapi-0.2.5 → certapi-0.2.8}/PKG-INFO +2 -2
  2. {certapi-0.2.5 → certapi-0.2.8}/README.md +1 -1
  3. {certapi-0.2.5 → certapi-0.2.8}/setup.py +1 -1
  4. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/Acme.py +11 -4
  5. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/certauthority.py +19 -8
  6. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/crypto.py +7 -0
  7. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/db.py +48 -21
  8. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/digitalocean_challenge_store.py +1 -1
  9. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/digitalocean_client.py +5 -6
  10. {certapi-0.2.5 → certapi-0.2.8}/src/certapi.egg-info/PKG-INFO +2 -2
  11. {certapi-0.2.5 → certapi-0.2.8}/MANIFEST.in +0 -0
  12. {certapi-0.2.5 → certapi-0.2.8}/pyproject.toml +0 -0
  13. {certapi-0.2.5 → certapi-0.2.8}/setup.cfg +0 -0
  14. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/__init__.py +0 -0
  15. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/challenge.py +0 -0
  16. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/cloudflare_challenge_store.py +0 -0
  17. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/cloudflare_client.py +0 -0
  18. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/crypto_classes.py +0 -0
  19. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/custom_certauthority.py +0 -0
  20. {certapi-0.2.5 → certapi-0.2.8}/src/certapi/util.py +0 -0
  21. {certapi-0.2.5 → certapi-0.2.8}/src/certapi.egg-info/SOURCES.txt +0 -0
  22. {certapi-0.2.5 → certapi-0.2.8}/src/certapi.egg-info/dependency_links.txt +0 -0
  23. {certapi-0.2.5 → certapi-0.2.8}/src/certapi.egg-info/requires.txt +0 -0
  24. {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.5
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 supposed to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
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 supposed to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
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
 
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="certapi",
5
- version="0.2.5",
5
+ version="0.2.8",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -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
- else:
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
- else:
109
+ elif err_detail:
107
110
  message = err_detail
108
- else:
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 ( certificate_res.content, certificate)
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 = order.get_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[0], missing)
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(existing: Dict[str, Tuple[int | str, Key, Certificate | str]], issued_certs: List["IssuedCert"]):
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
- cert_pem = cert if isinstance(cert, str) else cert_to_pem(cert).decode("utf-8")
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
- if isinstance(cert, Certificate):
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(self, private_key_id: int, cert: Certificate|str, domains: List[str], name: str = None) -> int:
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(self, private_key_id: int, cert: Certificate|str, domains: List[str], name: str = None) -> int:
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, cert_to_der(cert)),
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
- return res if res is None else (res[0], Key.from_der(res[1]), cert_from_der(res[2]))
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 cert_from_pem(cert_data)
198
+ return certs_from_pem(cert_data)
183
199
  return None
184
200
 
185
-
186
- def save_cert(self, private_key_id: str, cert: Certificate | str, domains: list, name: str = None) -> int:
187
- def get_cert_pem(cert) -> bytes:
188
- return cert.encode() if isinstance(cert, str) else cert_to_pem(cert)
189
-
190
- cert_pem = get_cert_pem(cert)
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 = cert_from_pem(f.read())
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(self, private_key_id: int, cert: Certificate, domains: List[str], name: str = None) -> int:
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, cert.public_bytes(serialization.Encoding.DER)),
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]), cert_from_der(res[2]))
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"] # DigitalOcean API returns 'data' for TXT record content
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.5
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 supposed to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
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