certapi 0.4.1__tar.gz → 0.4.3__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 (62) hide show
  1. {certapi-0.4.1 → certapi-0.4.3}/PKG-INFO +4 -4
  2. {certapi-0.4.1 → certapi-0.4.3}/README.md +3 -3
  3. {certapi-0.4.1 → certapi-0.4.3}/setup.py +1 -1
  4. certapi-0.4.3/src/certapi/__init__.py +23 -0
  5. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/Acme.py +11 -7
  6. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/AcmeError.py +3 -1
  7. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/Challenge.py +17 -18
  8. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/Order.py +3 -2
  9. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/__init__.py +1 -1
  10. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/http.py +1 -2
  11. certapi-0.4.1/src/certapi/challenge_store/ChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/ChallengeSolver.py +8 -6
  12. certapi-0.4.1/src/certapi/challenge_store/FileSystemChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/FileSystemChallengeSolver.py +9 -4
  13. certapi-0.4.1/src/certapi/challenge_store/InmemoryChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/InmemoryChallengeSolver.py +6 -2
  14. certapi-0.4.3/src/certapi/challenge_solver/__init__.py +24 -0
  15. certapi-0.4.3/src/certapi/challenge_solver/dns/__init__.py +2 -0
  16. certapi-0.4.1/src/certapi/challenge_store/dns/cloudflare/cloudflare_challenge_store.py → certapi-0.4.3/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +10 -7
  17. {certapi-0.4.1/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/cloudflare/cloudflare_client.py +6 -5
  18. certapi-0.4.1/src/certapi/challenge_store/dns/digitalocean/digitalocean_challenge_store.py → certapi-0.4.3/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +7 -3
  19. {certapi-0.4.1/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/digitalocean/digitalocean_client.py +1 -1
  20. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/client/cert_manager_client.py +29 -11
  21. certapi-0.4.3/src/certapi/crypto/__init__.py +2 -0
  22. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/crypto/crypto.py +4 -3
  23. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/crypto/crypto_classes.py +40 -12
  24. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/http/types.py +13 -6
  25. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/issuers/AcmeCertIssuer.py +57 -39
  26. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/issuers/SelfCertIssuer.py +3 -2
  27. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/issuers/__init__.py +1 -1
  28. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/issuers/abstract_certissuer.py +35 -26
  29. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/FileSystemKeystore.py +11 -8
  30. certapi-0.4.3/src/certapi/keystore/KeyStore.py +71 -0
  31. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/PostgresqlKeyStore.py +12 -5
  32. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/RemoteKeyStore.py +7 -3
  33. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/SqliteKeyStore.py +7 -4
  34. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/__init__.py +1 -1
  35. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/manager/acme_cert_manager.py +41 -44
  36. certapi-0.4.3/src/certapi/server/api.py +103 -0
  37. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/server/cert_api.py +48 -36
  38. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/server/key_api.py +25 -28
  39. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/utils.py +0 -1
  40. {certapi-0.4.1 → certapi-0.4.3}/src/certapi.egg-info/PKG-INFO +4 -4
  41. {certapi-0.4.1 → certapi-0.4.3}/src/certapi.egg-info/SOURCES.txt +11 -12
  42. certapi-0.4.1/src/certapi/__init__.py +0 -9
  43. certapi-0.4.1/src/certapi/challenge_store/FilesystemChallengeStore.py +0 -38
  44. certapi-0.4.1/src/certapi/challenge_store/__init__.py +0 -25
  45. certapi-0.4.1/src/certapi/challenge_store/dns/__init__.py +0 -2
  46. certapi-0.4.1/src/certapi/crypto/__init__.py +0 -2
  47. certapi-0.4.1/src/certapi/keystore/KeyStore.py +0 -66
  48. certapi-0.4.1/src/certapi/server/api.py +0 -89
  49. {certapi-0.4.1 → certapi-0.4.3}/MANIFEST.in +0 -0
  50. {certapi-0.4.1 → certapi-0.4.3}/pyproject.toml +0 -0
  51. {certapi-0.4.1 → certapi-0.4.3}/setup.cfg +0 -0
  52. {certapi-0.4.1/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/cloudflare/__init__.py +0 -0
  53. {certapi-0.4.1/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/digitalocean/__init__.py +0 -0
  54. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/client/__init__.py +0 -0
  55. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/http/__init__.py +0 -0
  56. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/http/client.py +0 -0
  57. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/manager/__init__.py +0 -0
  58. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/server/__init__.py +0 -0
  59. {certapi-0.4.1 → certapi-0.4.3}/src/certapi/util.py +0 -0
  60. {certapi-0.4.1 → certapi-0.4.3}/src/certapi.egg-info/dependency_links.txt +0 -0
  61. {certapi-0.4.1 → certapi-0.4.3}/src/certapi.egg-info/requires.txt +0 -0
  62. {certapi-0.4.1 → certapi-0.4.3}/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.4.1
3
+ Version: 0.4.3
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
@@ -30,12 +30,12 @@ pip install certapi
30
30
 
31
31
  ```python
32
32
  import json
33
- from certapi import FileSystemChallengeStore, FilesystemKeyStore, CertAuthority
33
+ from certapi import FileSystemChallengeSolver, FilesystemKeyStore, CertAuthority
34
34
 
35
35
  key_store = FilesystemKeyStore("data")
36
- challenge_store = FileSystemChallengeStore("./acme-challenges") # this should be where your web server hosts the .well-known/
36
+ challenge_solver = FileSystemChallengeSolver("./acme-challenges") # this should be where your web server hosts the .well-known/
37
37
 
38
- certAuthority = CertAuthority(challenge_store, key_store)
38
+ certAuthority = CertAuthority(challenge_solver, key_store)
39
39
  certAuthority.setup()
40
40
 
41
41
  (response,_) = certAuthority.obtainCert("example.com")
@@ -17,12 +17,12 @@ pip install certapi
17
17
 
18
18
  ```python
19
19
  import json
20
- from certapi import FileSystemChallengeStore, FilesystemKeyStore, CertAuthority
20
+ from certapi import FileSystemChallengeSolver, FilesystemKeyStore, CertAuthority
21
21
 
22
22
  key_store = FilesystemKeyStore("data")
23
- challenge_store = FileSystemChallengeStore("./acme-challenges") # this should be where your web server hosts the .well-known/
23
+ challenge_solver = FileSystemChallengeSolver("./acme-challenges") # this should be where your web server hosts the .well-known/
24
24
 
25
- certAuthority = CertAuthority(challenge_store, key_store)
25
+ certAuthority = CertAuthority(challenge_solver, key_store)
26
26
  certAuthority.setup()
27
27
 
28
28
  (response,_) = certAuthority.obtainCert("example.com")
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="certapi",
5
- version="0.4.1",
5
+ version="0.4.3",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -0,0 +1,23 @@
1
+ from .acme.Acme import Acme, Order, AcmeNetworkError, AcmeHttpError, Challenge
2
+ from .manager.acme_cert_manager import AcmeCertManager
3
+ from .http.types import CertificateResponse, IssuedCert
4
+
5
+ from .crypto import (
6
+ Certificate,
7
+ CertificateSigningRequest,
8
+ CertificateSigningRequestBuilder,
9
+ Key,
10
+ Ed25519Key,
11
+ ECDSAKey,
12
+ Ed25519PrivateKey,
13
+ EllipticCurvePrivateKey,
14
+ )
15
+ from .keystore import FileSystemKeystore, SqliteKeyStore, PostgresKeyStore, KeyStore
16
+ from .challenge_solver import (
17
+ ChallengeSolver,
18
+ InMemoryChallengeSolver,
19
+ FilesystemChallengeSolver,
20
+ CloudflareChallengeSolver,
21
+ DigitalOceanChallengeSolver,
22
+ )
23
+ from .issuers import SelfCertIssuer
@@ -9,23 +9,26 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
9
9
  from cryptography.x509 import CertificateSigningRequest, Certificate
10
10
  from certapi.crypto import crypto
11
11
  import requests
12
- from certapi.crypto import sign, digest_sha256, csr_to_der, jwk, get_algorithm_name, sign_for_jws,Key
12
+ from certapi.crypto import Key
13
+ from certapi.crypto.crypto_classes import ECDSAKey, RSAKey
13
14
  from certapi.util import b64_encode, b64_string
14
15
  from .AcmeError import *
15
16
  from .http import *
16
17
  from .Challenge import Challenge
17
18
  from .Order import Order
18
19
 
20
+
19
21
  class Acme:
20
22
 
21
23
  URL_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory"
22
24
  URL_PROD = "https://acme-v02.api.letsencrypt.org/directory"
23
25
 
24
- def __init__(self, account_key: Union[RSAPrivateKey, EllipticCurvePrivateKey | Key], url=URL_STAGING):
25
- self.account_key = account_key.key if isinstance(account_key, Key) else account_key
26
-
26
+ def __init__(self, account_key: Key, url=URL_STAGING):
27
+ self.account_key: Key = account_key
28
+
27
29
  # json web key format for public key
28
- self.jwk = jwk(self.account_key)
30
+ self.jwk = account_key.jwk()
31
+ self.alg_name = account_key.algorithm_name()
29
32
  print(self.jwk)
30
33
  self.nonce = []
31
34
  self.acme_url = url if url else os.environ.get("ACME_API_URL", self.URL_STAGING)
@@ -71,7 +74,7 @@ class Acme:
71
74
 
72
75
  protected = {
73
76
  "url": url,
74
- "alg": get_algorithm_name(self.account_key),
77
+ "alg": self.alg_name,
75
78
  "nonce": self.get_nonce(step),
76
79
  }
77
80
 
@@ -79,11 +82,12 @@ class Acme:
79
82
  protected["kid"] = self.key_id
80
83
  else:
81
84
  protected["jwk"] = self.jwk
85
+
82
86
  protectedb64 = b64_encode(protected)
83
87
  payload = {
84
88
  "protected": protectedb64.decode("utf-8"),
85
89
  "payload": payload64.decode("utf-8"),
86
- "signature": b64_string(sign_for_jws(self.account_key, b".".join([protectedb64, payload64]))),
90
+ "signature": b64_string(self.account_key.jws_sign(b".".join([protectedb64, payload64]))),
87
91
  }
88
92
  try:
89
93
 
@@ -1,3 +1,4 @@
1
+ import re
1
2
  import requests
2
3
 
3
4
 
@@ -47,6 +48,7 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
47
48
  failed_challenge: dict = [x for x in res_json["challenges"] if x["status"] == "invalid"][0]
48
49
  error = failed_challenge["error"]
49
50
  err_detail: str = error.get("detail")
51
+ err_type: str = error.get("type")
50
52
  validation_record: dict = failed_challenge.get("validationRecord")
51
53
  error = {
52
54
  "url": failed_challenge["url"],
@@ -70,7 +72,7 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
70
72
  "resolved": validation_record["addressesResolved"],
71
73
  "used": validation_record["addressUsed"],
72
74
  }
73
- if error["type"] == "urn:ietf:params:acme:error:connection":
75
+ if err_type == "urn:ietf:params:acme:error:connection":
74
76
  if "Timeout during connect" in err_detail:
75
77
  error["connect"] = {"error": "Timeout"}
76
78
  message = (
@@ -1,4 +1,3 @@
1
-
2
1
  import json
3
2
  from typing import Literal, Union
4
3
 
@@ -7,7 +6,8 @@ import requests
7
6
  from certapi.util import b64_encode, b64_string
8
7
  from certapi.crypto import digest_sha256
9
8
  from .AcmeError import *
10
- from .http import post,get
9
+ from .http import post, get
10
+
11
11
 
12
12
  class Challenge:
13
13
  def __init__(self, auth_url, data, acme):
@@ -15,7 +15,7 @@ class Challenge:
15
15
  self._acme = acme
16
16
  self._data = data
17
17
  challenge = self.get_challenge()
18
- self.token:str = challenge["token"]
18
+ self.token: str = challenge["token"]
19
19
  self.verified = challenge["status"] == "valid"
20
20
  self.domain = data["identifier"]["value"] # Add domain attribute
21
21
 
@@ -25,23 +25,22 @@ class Challenge:
25
25
 
26
26
  self.url = "http://{0}/.well-known/acme-challenge/{1}".format(data["identifier"]["value"], self.token)
27
27
 
28
- def as_key_value(self,type:Literal["http-01", "dns-01", "tls-alpn-01"]=None):
29
- challenge=self.get_challenge(type)
28
+ def as_key_value(self, type: Literal["http-01", "dns-01", "tls-alpn-01"] = None):
29
+ challenge = self.get_challenge(type)
30
30
 
31
31
  if challenge["type"] == "dns-01":
32
32
  key = f"_acme-challenge.{self.domain}"
33
33
  value = b64_string(digest_sha256(self.authorization_key.encode("utf8")))
34
34
  else:
35
- key = self.token
36
- value = self.authorization_key
37
-
38
- return (key,value)
35
+ key = self.token
36
+ value = self.authorization_key
39
37
 
38
+ return (key, value)
40
39
 
41
- def verify(self, type:Literal["http-01", "dns-01", "tls-alpn-01"]=None) -> bool:
40
+ def verify(self, type: Literal["http-01", "dns-01", "tls-alpn-01"] = None) -> bool:
42
41
  if not self.verified:
43
42
  response = self._acme._signed_req(
44
- self.get_challenge(type)["url"], {}, step="Verify Challenge", throw=False
43
+ self.get_challenge(type)["url"], {}, step=f"Verify Challenge [{self.domain}]", throw=False
45
44
  )
46
45
  if response.status_code == 200 and response.json()["status"] == "valid":
47
46
  self.verified = True
@@ -52,7 +51,7 @@ class Challenge:
52
51
  def self_verify(self) -> Union[bool, requests.Response]:
53
52
  identifier = self._data["identifier"]
54
53
  if identifier["type"] == "dns":
55
- res = get("Self Domain verification", self.url)
54
+ res = get(f"Self Domain verification [{self.domain}]", self.url)
56
55
  if res.status_code == 200 and res.content == self.token.encode():
57
56
  return True
58
57
  else:
@@ -63,19 +62,19 @@ class Challenge:
63
62
  if self.verified:
64
63
  return True
65
64
  else:
66
- res = self._acme._signed_req(self._auth_url, step="Acme Challenge Verification")
65
+ res = self._acme._signed_req(self._auth_url, step=f"Query Challenge Status [{self.domain}]")
67
66
  res_json = res.json()
68
67
  if res_json["status"] == "valid":
69
68
  self.verified = True
70
69
  return True
71
70
  elif res_json["status"] == "invalid":
72
- raise AcmeInvaliOrderError(res, "Acme Challenge Verification")
71
+ raise AcmeInvaliOrderError(res, "Query Challenge Status")
73
72
  else:
74
73
  return False
75
74
 
76
- def get_challenge(self, type:Literal["http-01", "dns-01", "tls-alpn-01"]=None):
75
+ def get_challenge(self, type: Literal["http-01", "dns-01", "tls-alpn-01"] = None):
77
76
  challenges = self._data["challenges"]
78
- def_key= type if type is not None else "http-01"
77
+ def_key = type if type is not None else "http-01"
79
78
  for method in challenges:
80
79
  if method["type"] == def_key:
81
80
  return method
@@ -84,7 +83,7 @@ class Challenge:
84
83
 
85
84
  ch_types = [x["type"] for x in self._data["challenges"]]
86
85
  raise AcmeError(
87
- f"'{type}' not found in challenges. available:{str(ch_types)}",
86
+ f"'{type}' not found in challenges for {self.domain}, available: {','.join(ch_types)}",
88
87
  {"response": self._data["challenges"]},
89
- "Acme Challenge Verification",
88
+ "Query Challenge Status",
90
89
  )
@@ -1,10 +1,11 @@
1
1
  from typing import List, Tuple
2
- from certapi.crypto import crypto,csr_to_der
2
+ from certapi.crypto import crypto, csr_to_der
3
3
  from certapi.util import b64_string
4
- from cryptography.x509 import CertificateSigningRequest,Certificate
4
+ from cryptography.x509 import CertificateSigningRequest, Certificate
5
5
  from .Challenge import *
6
6
  from .http import get
7
7
 
8
+
8
9
  class Order:
9
10
  def __init__(self, url, data, challenges, acme):
10
11
  self.url = url
@@ -1,4 +1,4 @@
1
1
  from .Challenge import Challenge
2
2
  from .AcmeError import *
3
3
  from .Acme import Acme
4
- from .Order import Order
4
+ from .Order import Order
@@ -1,7 +1,7 @@
1
-
2
1
  import requests
3
2
  from .AcmeError import *
4
3
 
4
+
5
5
  def request(method, step: str, url: str, json=None, headers=None, throw=True) -> requests.Response:
6
6
  res = None
7
7
  try:
@@ -53,4 +53,3 @@ def post(step: str, url: str, json=None, headers=None, throw=True) -> requests.R
53
53
 
54
54
  def get(step: str, url) -> requests.Response:
55
55
  return request("GET", step, url)
56
-
@@ -1,12 +1,14 @@
1
1
  from abc import ABC, abstractmethod
2
2
  import os
3
3
  from collections.abc import MutableMapping
4
+ from typing import Literal
4
5
 
5
6
 
6
- class ChallengeStore(ABC):
7
+ class ChallengeSolver(ABC):
7
8
  """
8
9
  Abstract base class for a challenge store.
9
10
  """
11
+
10
12
  @abstractmethod
11
13
  def supports_domain(self, domain: str) -> bool:
12
14
  pass
@@ -22,13 +24,13 @@ class ChallengeStore(ABC):
22
24
  @abstractmethod
23
25
  def delete_challenge(self, key: str, domain: str = None):
24
26
  pass
25
- def store_details():
26
- return {
27
- "name": "Abstract Challenge Store",
28
- }
27
+
28
+ @abstractmethod
29
+ def supported_challenge_type(self) -> Literal["http-01", "dns-01", "tls-alpn-01"]:
30
+ pass
29
31
 
30
32
  def __iter__(self):
31
33
  raise NotImplementedError("Must implement `__iter__` method.")
32
34
 
33
35
  def __len__(self):
34
- raise NotImplementedError("Must implement `__len__` method.")
36
+ raise NotImplementedError("Must implement `__len__` method.")
@@ -1,10 +1,12 @@
1
-
2
- from .ChallengeStore import ChallengeStore
1
+ from typing import Literal
2
+ from .ChallengeSolver import ChallengeSolver
3
3
  import os
4
4
 
5
- class FileSystemChallengeStore(ChallengeStore):
5
+
6
+ class FilesystemChallengeSolver(ChallengeSolver):
6
7
  """
7
8
  Filesystem implementation of the ChallengeStore.
9
+ This should never be used, but extended for your webserver e.g. nginx, apache etc.
8
10
  """
9
11
 
10
12
  def __init__(self, directory: str):
@@ -12,7 +14,7 @@ class FileSystemChallengeStore(ChallengeStore):
12
14
  os.makedirs(self.directory, exist_ok=True)
13
15
 
14
16
  def supports_domain(self, domain: str) -> bool:
15
- return True
17
+ return "*" not in domain
16
18
 
17
19
  def save_challenge(self, key: str, value: str, domain: str = None):
18
20
  file_path = os.path.join(self.directory, key)
@@ -31,6 +33,9 @@ class FileSystemChallengeStore(ChallengeStore):
31
33
  if os.path.exists(file_path):
32
34
  os.remove(file_path)
33
35
 
36
+ def supported_challenge_type(self) -> Literal["http-01"]:
37
+ return "http-01"
38
+
34
39
  def __iter__(self):
35
40
  return (f for f in os.listdir(self.directory) if os.path.isfile(os.path.join(self.directory, f)))
36
41
 
@@ -1,7 +1,8 @@
1
- from .ChallengeStore import ChallengeStore
1
+ from typing import Literal
2
+ from .ChallengeSolver import ChallengeSolver
2
3
 
3
4
 
4
- class InMemoryChallengeStore(ChallengeStore):
5
+ class InMemoryChallengeSolver(ChallengeSolver):
5
6
  """
6
7
  In-memory implementation of the ChallengeStore.
7
8
  """
@@ -9,6 +10,9 @@ class InMemoryChallengeStore(ChallengeStore):
9
10
  def __init__(self):
10
11
  self.challenges = {}
11
12
 
13
+ def supported_challenge_type(self) -> Literal["http-01"]:
14
+ return "http-01"
15
+
12
16
  def save_challenge(self, key: str, value: str, domain: str = None):
13
17
  self.challenges[key] = value
14
18
 
@@ -0,0 +1,24 @@
1
+ from .ChallengeSolver import ChallengeSolver
2
+ from .InmemoryChallengeSolver import InMemoryChallengeSolver
3
+ import os
4
+ from .FileSystemChallengeSolver import FilesystemChallengeSolver
5
+ from .dns import CloudflareChallengeSolver, DigitalOceanChallengeSolver
6
+
7
+
8
+ def get_challenge_solver():
9
+ """
10
+ Factory function to determine the type of store based on environment variables.
11
+
12
+ Environment Variables:
13
+ - `challenge_solver_TYPE`: Can be "memory" or "filesystem".
14
+ - `challenge_solver_DIR`: Directory for filesystem-based store. Defaults to "./challenges".
15
+ """
16
+ store_type = os.getenv("challenge_solver_TYPE", "filesystem").lower()
17
+ directory = os.getenv("challenge_solver_DIR", "./challenges")
18
+
19
+ if store_type == "memory":
20
+ return InMemoryChallengeSolver()
21
+ elif store_type == "filesystem":
22
+ return FilesystemChallengeSolver(directory)
23
+ else:
24
+ raise ValueError(f"Unknown challenge_solver_TYPE: {store_type}")
@@ -0,0 +1,2 @@
1
+ from .digitalocean.digitalocean_challenge_solver import DigitalOceanChallengeSolver
2
+ from .cloudflare.cloudflare_challenge_solver import CloudflareChallengeSolver
@@ -1,15 +1,19 @@
1
1
  import os
2
2
  import time
3
3
  from collections.abc import MutableMapping
4
- from ...ChallengeStore import ChallengeStore
4
+ from typing import Literal
5
+ from ...ChallengeSolver import ChallengeSolver
5
6
  from .cloudflare_client import Cloudflare
6
7
 
7
8
 
8
- class CloudflareChallengeStore(ChallengeStore):
9
- def __init__(self,api_key:str=None):
9
+ class CloudflareChallengeSolver(ChallengeSolver):
10
+ def __init__(self, api_key: str = None):
10
11
  self.cloudflare = Cloudflare(api_key)
11
- self.challenges_map = {}
12
-
12
+ self.challenges_map = {}
13
+
14
+ def supported_challenge_type(self) -> Literal["dns-01"]:
15
+ return "dns-01"
16
+
13
17
  def supports_domain(self, domain: str) -> bool:
14
18
  """
15
19
  Checks if the Cloudflare account has access to the given domain (or its base domain)
@@ -28,8 +32,7 @@ class CloudflareChallengeStore(ChallengeStore):
28
32
 
29
33
  record_id = self.cloudflare.create_record(name=key, data=value, domain=base_domain)
30
34
  self.challenges_map[key] = record_id
31
- print(f"CloudflareChallengeStore: Saved challenge for {key} with record ID {record_id}")
32
- time.sleep(10) # wait for dns propagation. it may not be immediate
35
+ print(f"CloudflareChallengeStore[{domain}]: Added Record {key}")
33
36
 
34
37
  def get_challenge(self, key: str, domain: str) -> str:
35
38
  base_domain = self.cloudflare.determine_registered_domain(domain)
@@ -7,7 +7,7 @@ from urllib.request import urlopen, Request
7
7
  class Cloudflare(object):
8
8
  name = "cloudflare"
9
9
 
10
- def __init__(self,api_key:str):
10
+ def __init__(self, api_key: str):
11
11
  self.token = api_key
12
12
  self.api = "https://api.cloudflare.com/client/v4"
13
13
  if not self.token:
@@ -85,6 +85,7 @@ class Cloudflare(object):
85
85
 
86
86
  result = json.loads(response.read().decode("utf8"))
87
87
  if not result.get("success"):
88
+ print(f"List TXT record [{response.getcode()}]", result)
88
89
  raise Exception(result.get("errors", "Unknown error listing TXT records"))
89
90
 
90
91
  return result["result"]
@@ -111,11 +112,11 @@ class Cloudflare(object):
111
112
  "proxied": False,
112
113
  }
113
114
  response = urlopen(Request(api_url, data=json.dumps(request_data).encode("utf8"), headers=request_headers))
114
-
115
+ result = response.read().decode("utf8")
115
116
  if response.getcode() != 200:
117
+ print(f"Create TXT record [{response.getcode()}]", result)
116
118
  raise Exception(json.loads(response.read().decode("utf8")))
117
- result = response.read().decode("utf8")
118
- print("Cloudflare create record", name, result)
119
+
119
120
  return json.loads(result)["result"]["id"]
120
121
 
121
122
  def delete_record(self, record, domain):
@@ -133,6 +134,6 @@ class Cloudflare(object):
133
134
  request.get_method = lambda: "DELETE"
134
135
  response = urlopen(request)
135
136
  result = response.read().decode("utf8")
136
- print(f"Delete dns record [{response.getcode()}]", result)
137
137
  if response.getcode() != 200:
138
+ print(f"Delete dns record [{response.getcode()}]", result)
138
139
  raise Exception(json.loads(response.read().decode("utf8")))
@@ -1,14 +1,18 @@
1
1
  import os
2
2
  from collections.abc import MutableMapping
3
- from ...ChallengeStore import ChallengeStore
3
+ from typing import Literal
4
+ from ...ChallengeSolver import ChallengeSolver
4
5
  from .digitalocean_client import DigitalOcean
5
6
 
6
7
 
7
- class DigitalOceanChallengeStore(ChallengeStore):
8
- def __init__(self,api_key:str=None):
8
+ class DigitalOceanChallengeSolver(ChallengeSolver):
9
+ def __init__(self, api_key: str = None):
9
10
  self.digitalocean = DigitalOcean(api_key)
10
11
  self.challenges_map = {} # Stores key: record_id (needed for deletion)
11
12
 
13
+ def supported_challenge_type(self) -> Literal["dns-01"]:
14
+ return "dns-01"
15
+
12
16
  def supports_domain(self, domain: str) -> bool:
13
17
  """
14
18
  Checks if the DigitalOcean account has access to the given domain (or its base domain)
@@ -5,7 +5,7 @@ from urllib.request import urlopen, Request # Python 3
5
5
 
6
6
 
7
7
  class DigitalOcean(object):
8
- def __init__(self, api_key:str=None):
8
+ def __init__(self, api_key: str = None):
9
9
  self.token = api_key
10
10
  self.api = "https://api.digitalocean.com/v2/domains"
11
11
  if not self.token:
@@ -2,6 +2,7 @@ import requests
2
2
  from typing import List, Union, Dict, Optional, Any
3
3
  from certapi.http.types import CertificateResponse, IssuedCert
4
4
 
5
+
5
6
  class CertManagerClient:
6
7
  def __init__(self, base_url: str):
7
8
  self.base_url = base_url
@@ -11,8 +12,15 @@ class CertManagerClient:
11
12
  response.raise_for_status()
12
13
  return response.json()
13
14
 
14
- def _post(self, path: str, data: Optional[Union[Dict[str, Any], str]] = None, headers: Optional[Dict[str, str]] = None) -> Any:
15
- response = requests.post(f"{self.base_url}{path}", json=data if isinstance(data, dict) else None, data=data if isinstance(data, str) else None, headers=headers)
15
+ def _post(
16
+ self, path: str, data: Optional[Union[Dict[str, Any], str]] = None, headers: Optional[Dict[str, str]] = None
17
+ ) -> Any:
18
+ response = requests.post(
19
+ f"{self.base_url}{path}",
20
+ json=data if isinstance(data, dict) else None,
21
+ data=data if isinstance(data, str) else None,
22
+ headers=headers,
23
+ )
16
24
  response.raise_for_status()
17
25
  return response.json()
18
26
 
@@ -32,17 +40,24 @@ class CertManagerClient:
32
40
  "key_type": key_type,
33
41
  "expiry_days": expiry_days,
34
42
  }
35
- if country: params["country"] = country
36
- if state: params["state"] = state
37
- if locality: params["locality"] = locality
38
- if organization: params["organization"] = organization
39
- if user_id: params["user_id"] = user_id
43
+ if country:
44
+ params["country"] = country
45
+ if state:
46
+ params["state"] = state
47
+ if locality:
48
+ params["locality"] = locality
49
+ if organization:
50
+ params["organization"] = organization
51
+ if user_id:
52
+ params["user_id"] = user_id
40
53
 
41
54
  data = self._get("/obtain", params=params)
42
55
  return CertificateResponse.from_json(data)
43
56
 
44
57
  def sign_csr(self, csr_pem: str) -> str:
45
- response = requests.post(f"{self.base_url}/sign_csr", data=csr_pem, headers={'Content-Type': 'application/x-pem-file'})
58
+ response = requests.post(
59
+ f"{self.base_url}/sign_csr", data=csr_pem, headers={"Content-Type": "application/x-pem-file"}
60
+ )
46
61
  response.raise_for_status()
47
62
  return response.text
48
63
 
@@ -88,11 +103,14 @@ class CertManagerClient:
88
103
  return None
89
104
  raise
90
105
 
91
- def save_cert(self, private_key_id: Union[int, str], cert_pem: str, domains: List[str], name: Optional[str] = None) -> Dict[str, Any]:
106
+ def save_cert(
107
+ self, private_key_id: Union[int, str], cert_pem: str, domains: List[str], name: Optional[str] = None
108
+ ) -> Dict[str, Any]:
92
109
  data = {
93
110
  "private_key_id": private_key_id,
94
111
  "cert": cert_pem,
95
112
  "domains": domains,
96
113
  }
97
- if name: data["name"] = name
98
- return self._post("/certs", data=data)
114
+ if name:
115
+ data["name"] = name
116
+ return self._post("/certs", data=data)
@@ -0,0 +1,2 @@
1
+ from .crypto import *
2
+ from .crypto_classes import *
@@ -10,11 +10,11 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
10
10
  from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
11
11
  from cryptography.x509 import Certificate, CertificateSigningRequestBuilder, CertificateSigningRequest
12
12
 
13
- from cryptography.x509.oid import NameOID,ExtensionOID
13
+ from cryptography.x509.oid import NameOID, ExtensionOID
14
14
  from cryptography.hazmat.primitives import hashes
15
15
 
16
16
 
17
- from certapi.util import b64_string
17
+ from certapi.util import b64_string
18
18
 
19
19
  __no_enc = serialization.NoEncryption()
20
20
 
@@ -177,6 +177,7 @@ def digest_sha256(data: bytes) -> bytes:
177
177
  h.update(data)
178
178
  return h.finalize()
179
179
 
180
+
180
181
  def get_csr_hostnames(csr: x509.CertificateSigningRequest):
181
182
 
182
183
  domains = []
@@ -203,4 +204,4 @@ def get_csr_hostnames(csr: x509.CertificateSigningRequest):
203
204
  seen.add(d)
204
205
  unique_domains.append(d)
205
206
 
206
- return unique_domains
207
+ return unique_domains