certapi 0.4.2__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 (57) hide show
  1. {certapi-0.4.2 → certapi-0.4.3}/PKG-INFO +4 -4
  2. {certapi-0.4.2 → certapi-0.4.3}/README.md +3 -3
  3. {certapi-0.4.2 → certapi-0.4.3}/setup.py +1 -1
  4. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/__init__.py +6 -6
  5. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/Acme.py +9 -6
  6. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/AcmeError.py +3 -1
  7. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/Challenge.py +8 -6
  8. certapi-0.4.2/src/certapi/challenge_store/ChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/ChallengeSolver.py +5 -5
  9. certapi-0.4.2/src/certapi/challenge_store/FileSystemChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/FileSystemChallengeSolver.py +8 -3
  10. certapi-0.4.2/src/certapi/challenge_store/InmemoryChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/InmemoryChallengeSolver.py +6 -2
  11. certapi-0.4.3/src/certapi/challenge_solver/__init__.py +24 -0
  12. certapi-0.4.3/src/certapi/challenge_solver/dns/__init__.py +2 -0
  13. certapi-0.4.2/src/certapi/challenge_store/dns/cloudflare/cloudflare_challenge_store.py → certapi-0.4.3/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +7 -4
  14. {certapi-0.4.2/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/cloudflare/cloudflare_client.py +5 -4
  15. certapi-0.4.2/src/certapi/challenge_store/dns/digitalocean/digitalocean_challenge_store.py → certapi-0.4.3/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +6 -2
  16. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/crypto/crypto_classes.py +36 -8
  17. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/issuers/AcmeCertIssuer.py +37 -30
  18. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/issuers/abstract_certissuer.py +17 -16
  19. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/FileSystemKeystore.py +4 -1
  20. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/KeyStore.py +3 -3
  21. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/manager/acme_cert_manager.py +14 -23
  22. {certapi-0.4.2 → certapi-0.4.3}/src/certapi.egg-info/PKG-INFO +4 -4
  23. {certapi-0.4.2 → certapi-0.4.3}/src/certapi.egg-info/SOURCES.txt +11 -11
  24. certapi-0.4.2/src/certapi/challenge_store/__init__.py +0 -24
  25. certapi-0.4.2/src/certapi/challenge_store/dns/__init__.py +0 -2
  26. {certapi-0.4.2 → certapi-0.4.3}/MANIFEST.in +0 -0
  27. {certapi-0.4.2 → certapi-0.4.3}/pyproject.toml +0 -0
  28. {certapi-0.4.2 → certapi-0.4.3}/setup.cfg +0 -0
  29. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/Order.py +0 -0
  30. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/__init__.py +0 -0
  31. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/http.py +0 -0
  32. {certapi-0.4.2/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/cloudflare/__init__.py +0 -0
  33. {certapi-0.4.2/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/digitalocean/__init__.py +0 -0
  34. {certapi-0.4.2/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/digitalocean/digitalocean_client.py +0 -0
  35. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/client/__init__.py +0 -0
  36. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/client/cert_manager_client.py +0 -0
  37. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/crypto/__init__.py +0 -0
  38. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/crypto/crypto.py +0 -0
  39. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/http/__init__.py +0 -0
  40. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/http/client.py +0 -0
  41. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/http/types.py +0 -0
  42. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/issuers/SelfCertIssuer.py +0 -0
  43. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/issuers/__init__.py +0 -0
  44. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
  45. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/RemoteKeyStore.py +0 -0
  46. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/SqliteKeyStore.py +0 -0
  47. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/__init__.py +0 -0
  48. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/manager/__init__.py +0 -0
  49. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/server/__init__.py +0 -0
  50. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/server/api.py +0 -0
  51. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/server/cert_api.py +0 -0
  52. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/server/key_api.py +0 -0
  53. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/util.py +0 -0
  54. {certapi-0.4.2 → certapi-0.4.3}/src/certapi/utils.py +0 -0
  55. {certapi-0.4.2 → certapi-0.4.3}/src/certapi.egg-info/dependency_links.txt +0 -0
  56. {certapi-0.4.2 → certapi-0.4.3}/src/certapi.egg-info/requires.txt +0 -0
  57. {certapi-0.4.2 → 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.2
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.2",
5
+ version="0.4.3",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -13,11 +13,11 @@ from .crypto import (
13
13
  EllipticCurvePrivateKey,
14
14
  )
15
15
  from .keystore import FileSystemKeystore, SqliteKeyStore, PostgresKeyStore, KeyStore
16
- from .challenge_store import (
17
- ChallengeStore,
18
- InMemoryChallengeStore,
19
- FileSystemChallengeStore,
20
- CloudflareChallengeStore,
21
- DigitalOceanChallengeStore,
16
+ from .challenge_solver import (
17
+ ChallengeSolver,
18
+ InMemoryChallengeSolver,
19
+ FilesystemChallengeSolver,
20
+ CloudflareChallengeSolver,
21
+ DigitalOceanChallengeSolver,
22
22
  )
23
23
  from .issuers import SelfCertIssuer
@@ -9,7 +9,8 @@ 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 *
@@ -22,11 +23,12 @@ class Acme:
22
23
  URL_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory"
23
24
  URL_PROD = "https://acme-v02.api.letsencrypt.org/directory"
24
25
 
25
- def __init__(self, account_key: Union[RSAPrivateKey, EllipticCurvePrivateKey | Key], url=URL_STAGING):
26
- self.account_key = account_key.key if isinstance(account_key, Key) else account_key
26
+ def __init__(self, account_key: Key, url=URL_STAGING):
27
+ self.account_key: Key = account_key
27
28
 
28
29
  # json web key format for public key
29
- self.jwk = jwk(self.account_key)
30
+ self.jwk = account_key.jwk()
31
+ self.alg_name = account_key.algorithm_name()
30
32
  print(self.jwk)
31
33
  self.nonce = []
32
34
  self.acme_url = url if url else os.environ.get("ACME_API_URL", self.URL_STAGING)
@@ -72,7 +74,7 @@ class Acme:
72
74
 
73
75
  protected = {
74
76
  "url": url,
75
- "alg": get_algorithm_name(self.account_key),
77
+ "alg": self.alg_name,
76
78
  "nonce": self.get_nonce(step),
77
79
  }
78
80
 
@@ -80,11 +82,12 @@ class Acme:
80
82
  protected["kid"] = self.key_id
81
83
  else:
82
84
  protected["jwk"] = self.jwk
85
+
83
86
  protectedb64 = b64_encode(protected)
84
87
  payload = {
85
88
  "protected": protectedb64.decode("utf-8"),
86
89
  "payload": payload64.decode("utf-8"),
87
- "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]))),
88
91
  }
89
92
  try:
90
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 = (
@@ -39,7 +39,9 @@ class Challenge:
39
39
 
40
40
  def verify(self, type: Literal["http-01", "dns-01", "tls-alpn-01"] = None) -> bool:
41
41
  if not self.verified:
42
- response = self._acme._signed_req(self.get_challenge(type)["url"], {}, step="Verify Challenge", throw=False)
42
+ response = self._acme._signed_req(
43
+ self.get_challenge(type)["url"], {}, step=f"Verify Challenge [{self.domain}]", throw=False
44
+ )
43
45
  if response.status_code == 200 and response.json()["status"] == "valid":
44
46
  self.verified = True
45
47
  return True
@@ -49,7 +51,7 @@ class Challenge:
49
51
  def self_verify(self) -> Union[bool, requests.Response]:
50
52
  identifier = self._data["identifier"]
51
53
  if identifier["type"] == "dns":
52
- res = get("Self Domain verification", self.url)
54
+ res = get(f"Self Domain verification [{self.domain}]", self.url)
53
55
  if res.status_code == 200 and res.content == self.token.encode():
54
56
  return True
55
57
  else:
@@ -60,13 +62,13 @@ class Challenge:
60
62
  if self.verified:
61
63
  return True
62
64
  else:
63
- 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}]")
64
66
  res_json = res.json()
65
67
  if res_json["status"] == "valid":
66
68
  self.verified = True
67
69
  return True
68
70
  elif res_json["status"] == "invalid":
69
- raise AcmeInvaliOrderError(res, "Acme Challenge Verification")
71
+ raise AcmeInvaliOrderError(res, "Query Challenge Status")
70
72
  else:
71
73
  return False
72
74
 
@@ -81,7 +83,7 @@ class Challenge:
81
83
 
82
84
  ch_types = [x["type"] for x in self._data["challenges"]]
83
85
  raise AcmeError(
84
- f"'{type}' not found in challenges. available:{str(ch_types)}",
86
+ f"'{type}' not found in challenges for {self.domain}, available: {','.join(ch_types)}",
85
87
  {"response": self._data["challenges"]},
86
- "Acme Challenge Verification",
88
+ "Query Challenge Status",
87
89
  )
@@ -1,9 +1,10 @@
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
  """
@@ -24,10 +25,9 @@ class ChallengeStore(ABC):
24
25
  def delete_challenge(self, key: str, domain: str = None):
25
26
  pass
26
27
 
27
- def store_details():
28
- return {
29
- "name": "Abstract Challenge Store",
30
- }
28
+ @abstractmethod
29
+ def supported_challenge_type(self) -> Literal["http-01", "dns-01", "tls-alpn-01"]:
30
+ pass
31
31
 
32
32
  def __iter__(self):
33
33
  raise NotImplementedError("Must implement `__iter__` method.")
@@ -1,10 +1,12 @@
1
- from .ChallengeStore import ChallengeStore
1
+ from typing import Literal
2
+ from .ChallengeSolver import ChallengeSolver
2
3
  import os
3
4
 
4
5
 
5
- class FileSystemChallengeStore(ChallengeStore):
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
+ class CloudflareChallengeSolver(ChallengeSolver):
9
10
  def __init__(self, api_key: str = None):
10
11
  self.cloudflare = Cloudflare(api_key)
11
12
  self.challenges_map = {}
12
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)
@@ -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
+ class DigitalOceanChallengeSolver(ChallengeSolver):
8
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)
@@ -1,10 +1,11 @@
1
1
  from abc import ABC, abstractmethod
2
- from cryptography.hazmat.primitives.asymmetric import rsa, ed25519, ec
3
- from cryptography.hazmat.primitives import serialization, hashes, padding
2
+ from cryptography.hazmat.primitives.asymmetric import rsa, ed25519, ec, padding
3
+ from cryptography.hazmat.primitives import serialization, hashes
4
4
  from typing import Literal, Optional, List, Union
5
5
  from cryptography import x509
6
6
  from cryptography.x509.oid import NameOID
7
7
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
8
+ from cryptography.hazmat.primitives.asymmetric import utils
8
9
 
9
10
  from .crypto import key_to_der, key_to_pem
10
11
  from certapi.util import b64_string
@@ -18,9 +19,16 @@ class Key(ABC):
18
19
  pass
19
20
 
20
21
  @abstractmethod
21
- def sign(self, message: bytes):
22
+ def algorithm_name(self) -> str:
22
23
  pass
23
24
 
25
+ @abstractmethod
26
+ def sign(self, message: bytes) -> bytes:
27
+ pass
28
+
29
+ def jws_sign(self, message: bytes) -> bytes:
30
+ return self.sign(message)
31
+
24
32
  @abstractmethod
25
33
  def sign_csr(self, csr: x509.CertificateSigningRequestBuilder | x509.CertificateBuilder):
26
34
  pass
@@ -183,7 +191,7 @@ class Ed25519Key(Key):
183
191
  public = self.key.public_key().public_bytes(
184
192
  encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
185
193
  )
186
- self.jwk = {
194
+ self._jwk = {
187
195
  "crv": "Ed25519",
188
196
  "kty": "OKP",
189
197
  "x": b64_string(public),
@@ -195,11 +203,14 @@ class Ed25519Key(Key):
195
203
  return Ed25519Key(key)
196
204
 
197
205
  def jwk(self):
198
- return self.jwk
206
+ return self._jwk
199
207
 
200
208
  def sign(self, message):
201
209
  return self.key.sign(message)
202
210
 
211
+ def algorithm_name(self):
212
+ return "EdDSA"
213
+
203
214
  def sign_csr(self, csr):
204
215
  return csr.sign(self.key, None)
205
216
 
@@ -209,7 +220,7 @@ class ECDSAKey(Key):
209
220
  self.key = key
210
221
  public_key = self.key.public_key()
211
222
  public_numbers = public_key.public_numbers()
212
- self.jwk = {
223
+ self._jwk = {
213
224
  "kty": "EC",
214
225
  "crv": self.key.curve.name,
215
226
  "x": b64_string(public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, "big")),
@@ -222,10 +233,18 @@ class ECDSAKey(Key):
222
233
  return ECDSAKey(key)
223
234
 
224
235
  def jwk(self):
225
- return self.jwk
236
+ return self._jwk
226
237
 
227
238
  def algorithm_name(self):
228
- return self.key.curve.name
239
+ curve_name = self.key.curve.name
240
+ if curve_name == "secp256r1":
241
+ return "ES256"
242
+ elif curve_name == "secp384r1":
243
+ return "ES384"
244
+ elif curve_name == "secp521r1":
245
+ return "ES512"
246
+ else:
247
+ raise ValueError(f"Unsupported EC curve: {curve_name}")
229
248
 
230
249
  def sign(self, message):
231
250
  key_size = self.key.curve.key_size
@@ -239,6 +258,15 @@ class ECDSAKey(Key):
239
258
  raise ValueError(f"Unsupported curve with key size {key_size}")
240
259
  return self.key.sign(message, ec.ECDSA(algorithm))
241
260
 
261
+ def jws_sign(self, message: bytes) -> bytes:
262
+ print("Jws sign")
263
+ der_sig = self.sign(message)
264
+ r, s = utils.decode_dss_signature(der_sig)
265
+ num_bytes = (self.key.curve.key_size + 7) // 8
266
+ r_bytes = r.to_bytes(num_bytes, "big")
267
+ s_bytes = s.to_bytes(num_bytes, "big")
268
+ return r_bytes + s_bytes
269
+
242
270
  def sign_csr(self, csr):
243
271
  key_size = self.key.curve.key_size
244
272
  if key_size == 256:
@@ -1,4 +1,4 @@
1
- from certapi.crypto import ECDSAKey, Ed25519Key, RSAKey, Key
1
+ from certapi.crypto import Key
2
2
  from .abstract_certissuer import CertIssuer
3
3
  from cryptography import x509
4
4
  from cryptography.x509 import Certificate
@@ -6,48 +6,55 @@ from typing import List, Literal, Union, Callable, Tuple, Dict, Optional
6
6
  import time
7
7
  from requests import Response
8
8
  from certapi.acme import Acme, Challenge, Order
9
- from certapi.challenge_store import ChallengeStore
9
+ from certapi.challenge_solver import ChallengeSolver
10
10
 
11
11
 
12
12
  class AcmeCertIssuer(CertIssuer):
13
13
  def __init__(
14
14
  self,
15
15
  account_key: Key,
16
- challenge_store: ChallengeStore,
16
+ challenge_solver: ChallengeSolver,
17
17
  acme_url=None,
18
18
  self_verify_challenge=False, # This never needs to be set to True
19
19
  ):
20
20
  self.acme = Acme(account_key, url=acme_url)
21
- self.challenge_store = challenge_store
21
+ self.challenge_solver = challenge_solver
22
22
  self.self_verify_challenge = self_verify_challenge
23
23
 
24
24
  def setup(self):
25
25
  self.acme.setup()
26
26
  res: Response = self.acme.register()
27
27
  if res.status_code == 201:
28
- print("Acme Account was already registered")
28
+ print("New Acme Account registered")
29
29
  elif res.status_code != 200:
30
30
  raise Exception("Acme registration didn't return 200 or 201 ", res.json())
31
31
 
32
32
  def sign_csr(
33
- self, csr: x509.CertificateSigningRequest, challenge_store: ChallengeStore = None, expiry_days: int = 90
33
+ self, csr: x509.CertificateSigningRequest, challenge_solver: ChallengeSolver = None, expiry_days: int = 90
34
34
  ) -> str:
35
- challenge_store = challenge_store if challenge_store is not None else self.challenge_store
35
+ challenge_solver = challenge_solver if challenge_solver is not None else self.challenge_solver
36
36
  hosts = self.get_csr_hostnames(csr)
37
37
  order: Order = self.acme.create_authorized_order(hosts)
38
38
  challenges = order.remaining_challenges()
39
39
 
40
40
  for c in challenges:
41
- key, value = c.as_key_value()
42
- challenge_store.save_challenge(key, value, c.domain)
41
+ key, value = c.as_key_value(type=challenge_solver.supported_challenge_type())
42
+ challenge_solver.save_challenge(key, value, c.domain)
43
43
  for c in challenges:
44
44
  if self.self_verify_challenge:
45
45
  c.self_verify()
46
+
46
47
  end = time.time() + max(len(challenges) * 10, 300)
47
48
  remaining_now: List[Challenge] = [x for x in challenges]
48
49
  next_remaining = []
49
50
  counter = 1
50
51
 
52
+ if challenge_solver.supported_challenge_type() == "dns-01":
53
+ print("Waiting 10 seconds for DNS propagation .. ")
54
+ time.sleep(10) # sleep 7 seconds for dns propagation
55
+
56
+ for c in challenges:
57
+ c.verify(challenge_solver.supported_challenge_type())
51
58
  while len(remaining_now) > 0:
52
59
  if time.time() > end and counter > 4:
53
60
  print("Order finalization time out")
@@ -67,13 +74,13 @@ class AcmeCertIssuer(CertIssuer):
67
74
 
68
75
  if order.status == "valid":
69
76
  for c in challenges:
70
- challenge_store.delete_challenge(key, c.domain)
77
+ challenge_solver.delete_challenge(key, c.domain)
71
78
  return order.get_certificate()
72
79
  elif order.status == "processing":
73
80
  if count == 0:
74
81
  # Clean up challenges if timeout occurs
75
82
  for c in challenges:
76
- challenge_store.delete_challenge(key, c.domain)
83
+ challenge_solver.delete_challenge(key, c.domain)
77
84
  return None
78
85
  return obtain_cert()
79
86
  return None
@@ -90,12 +97,21 @@ class AcmeCertIssuer(CertIssuer):
90
97
  locality: Optional[str] = None,
91
98
  organization: Optional[str] = None,
92
99
  user_id: Optional[str] = None,
93
- challenge_store: Optional[ChallengeStore] = None,
94
- ):
100
+ challenge_solver: Optional[ChallengeSolver] = None,
101
+ ) -> Tuple[Key, str]:
95
102
  if len(hosts) == 0:
96
103
  raise ValueError("CertIssuer.generate_key_and_cert_for_domains: empty hosts array provided")
97
104
  return self.generate_key_and_cert(
98
- hosts[0], hosts[0:], key_type, expiry_days, country, state, locality, organization, user_id, challenge_store
105
+ hosts[0],
106
+ hosts[0:],
107
+ key_type,
108
+ expiry_days,
109
+ country,
110
+ state,
111
+ locality,
112
+ organization,
113
+ user_id,
114
+ challenge_solver,
99
115
  )
100
116
 
101
117
  def generate_key_and_cert_for_domain(
@@ -108,11 +124,11 @@ class AcmeCertIssuer(CertIssuer):
108
124
  locality: Optional[str] = None,
109
125
  organization: Optional[str] = None,
110
126
  user_id: Optional[str] = None,
111
- challenge_store: Optional[ChallengeStore] = None,
112
- ):
127
+ challenge_solver: Optional[ChallengeSolver] = None,
128
+ ) -> Tuple[Key, str]:
113
129
 
114
130
  return self.generate_key_and_cert(
115
- host, [], key_type, expiry_days, country, state, locality, organization, user_id, challenge_store
131
+ host, [], key_type, expiry_days, country, state, locality, organization, user_id, challenge_solver
116
132
  )
117
133
 
118
134
  def generate_key_and_cert(
@@ -126,19 +142,10 @@ class AcmeCertIssuer(CertIssuer):
126
142
  locality: Optional[str] = None,
127
143
  organization: Optional[str] = None,
128
144
  user_id: Optional[str] = None,
129
- challenge_store: Optional[ChallengeStore] = None,
130
- ) -> tuple:
145
+ challenge_solver: Optional[ChallengeSolver] = None,
146
+ ) -> Tuple[Key, str]:
131
147
  """Create a new certificate with a generated key."""
132
- # Generate new key based on key_type
133
- if key_type == "rsa":
134
- new_key = RSAKey.generate()
135
- elif key_type == "ecdsa":
136
- new_key = ECDSAKey.generate()
137
- elif key_type == "ed25519":
138
- new_key = Ed25519Key.generate()
139
- else:
140
- raise ValueError("Unsupported key type. Use 'rsa' or 'ecdsa'")
141
-
148
+ new_key = Key.generate(key_type)
142
149
  # Create CSR using the new key
143
150
  csr = new_key.create_csr(
144
151
  domain=domain,
@@ -151,6 +158,6 @@ class AcmeCertIssuer(CertIssuer):
151
158
  )
152
159
 
153
160
  # Sign the CSR to get the certificate
154
- cert = self.sign_csr(csr, expiry_days=expiry_days, challenge_store=challenge_store)
161
+ cert = self.sign_csr(csr, expiry_days=expiry_days, challenge_solver=challenge_solver)
155
162
 
156
163
  return new_key, cert
@@ -3,8 +3,8 @@ from typing import List, Literal, Optional, Union
3
3
  from cryptography import x509
4
4
  from cryptography.x509.oid import NameOID, ExtensionOID
5
5
  from certapi.crypto.crypto import get_csr_hostnames
6
- from certapi.crypto.crypto_classes import ECDSAKey, Ed25519Key, RSAKey
7
- from ..challenge_store import ChallengeStore
6
+ from certapi.crypto.crypto_classes import ECDSAKey, Ed25519Key, Key, RSAKey
7
+ from ..challenge_solver import ChallengeSolver
8
8
 
9
9
 
10
10
  class CertIssuer(ABC):
@@ -37,12 +37,21 @@ class CertIssuer(ABC):
37
37
  locality: Optional[str] = None,
38
38
  organization: Optional[str] = None,
39
39
  user_id: Optional[str] = None,
40
- challenge_store: Optional[ChallengeStore] = None,
40
+ challenge_solver: Optional[ChallengeSolver] = None,
41
41
  ):
42
42
  if len(hosts) == 0:
43
43
  raise ValueError("CertIssuer.generate_key_and_cert_for_domains: empty hosts array provided")
44
44
  return self.generate_key_and_cert(
45
- hosts[0], hosts[0:], key_type, expiry_days, country, state, locality, organization, user_id, challenge_store
45
+ hosts[0],
46
+ hosts[0:],
47
+ key_type,
48
+ expiry_days,
49
+ country,
50
+ state,
51
+ locality,
52
+ organization,
53
+ user_id,
54
+ challenge_solver,
46
55
  )
47
56
 
48
57
  def generate_key_and_cert_for_domain(
@@ -55,11 +64,11 @@ class CertIssuer(ABC):
55
64
  locality: Optional[str] = None,
56
65
  organization: Optional[str] = None,
57
66
  user_id: Optional[str] = None,
58
- challenge_store: Optional[ChallengeStore] = None,
67
+ challenge_solver: Optional[ChallengeSolver] = None,
59
68
  ):
60
69
 
61
70
  return self.generate_key_and_cert(
62
- host, [], key_type, expiry_days, country, state, locality, organization, user_id, challenge_store
71
+ host, [], key_type, expiry_days, country, state, locality, organization, user_id, challenge_solver
63
72
  )
64
73
 
65
74
  def generate_key_and_cert(
@@ -73,18 +82,10 @@ class CertIssuer(ABC):
73
82
  locality: Optional[str] = None,
74
83
  organization: Optional[str] = None,
75
84
  user_id: Optional[str] = None,
76
- challenge_store: Optional[ChallengeStore] = None,
85
+ challenge_solver: Optional[ChallengeSolver] = None,
77
86
  ) -> tuple:
78
87
  """Create a new certificate with a generated key."""
79
- # Generate new key based on key_type
80
- if key_type == "rsa":
81
- new_key = RSAKey.generate()
82
- elif key_type == "ecdsa":
83
- new_key = ECDSAKey.generate()
84
- elif key_type == "ed25519":
85
- new_key = Ed25519Key.generate()
86
- else:
87
- raise ValueError("Unsupported key type. Use 'rsa' or 'ecdsa'")
88
+ new_key = Key.generate(key_type)
88
89
 
89
90
  # Create CSR using the new key
90
91
  csr = new_key.create_csr(
@@ -30,7 +30,10 @@ class FileSystemKeystore(KeyStore):
30
30
  if os.path.exists(key_path):
31
31
  with open(key_path, "rb") as f:
32
32
  key_data = f.read()
33
- return Key.from_pem(key_data)
33
+ try:
34
+ return Key.from_pem(key_data)
35
+ except:
36
+ pass
34
37
  return None
35
38
 
36
39
  def find_key_by_name(self, name: str) -> Optional[Key]:
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  import sqlite3
3
- from typing import Tuple, Optional, Union, List
3
+ from typing import Literal, Tuple, Optional, Union, List
4
4
  from contextlib import contextmanager
5
5
 
6
6
  from abc import ABC, abstractmethod
@@ -9,10 +9,10 @@ from certapi.crypto import Key, Certificate, certs_to_pem, cert_to_pem, certs_fr
9
9
 
10
10
 
11
11
  class KeyStore(ABC):
12
- def _get_or_generate_key(self, id: str | int) -> Key:
12
+ def _get_or_generate_key(self, id: str | int, key_type: Literal["rsa", "ecdsa", "ed25519"] = "ecdsa") -> Key:
13
13
  account_key = self.find_key_by_id(id)
14
14
  if account_key is None:
15
- account_key = Key.generate("ecdsa")
15
+ account_key = Key.generate(key_type)
16
16
  id = self.save_key(account_key, id)
17
17
  return (account_key, id)
18
18
 
@@ -3,7 +3,7 @@ from typing import List, Literal, Optional, Tuple, Union, Dict
3
3
 
4
4
  from certapi import crypto
5
5
  from ..acme import Challenge
6
- from ..challenge_store import ChallengeStore
6
+ from ..challenge_solver import ChallengeSolver
7
7
 
8
8
  from ..issuers import AcmeCertIssuer, CertIssuer
9
9
  from ..http.types import CertificateResponse, IssuedCert
@@ -17,12 +17,12 @@ class AcmeCertManager:
17
17
  self,
18
18
  key_store: KeyStore,
19
19
  cert_issuer: AcmeCertIssuer,
20
- challenge_stores: List[ChallengeStore] = [],
20
+ challenge_solvers: List[ChallengeSolver] = [],
21
21
  ):
22
22
  self.key_store: KeyStore = key_store
23
23
  self.cert_issuer: AcmeCertIssuer = cert_issuer
24
- self.challenge_stores: List[ChallengeStore] = challenge_stores
25
- print(f"AcmeCertManager initialized with challenge_stores: {self.challenge_stores}")
24
+ self.challenge_solvers: List[ChallengeSolver] = challenge_solvers
25
+ print(f"AcmeCertManager initialized with challenge_solvers: {self.challenge_solvers}")
26
26
 
27
27
  def setup(self):
28
28
  self.cert_issuer.setup()
@@ -36,16 +36,16 @@ class AcmeCertManager:
36
36
  raise ValueError("CSR does not contain any hostnames.")
37
37
 
38
38
  # Find a challenge store that supports all hostnames in the CSR
39
- selected_challenge_store = None
40
- for store in self.challenge_stores:
39
+ selected_challenge_solver = None
40
+ for store in self.challenge_solvers:
41
41
  if all(store.supports_domain(h) for h in hostnames):
42
- selected_challenge_store = store
42
+ selected_challenge_solver = store
43
43
  break
44
44
 
45
- if selected_challenge_store is None:
45
+ if selected_challenge_solver is None:
46
46
  raise ValueError(f"No challenge store found that supports all domains: {hostnames}")
47
47
 
48
- fullchain_cert = self.cert_issuer.sign_csr(csr, challenge_store=selected_challenge_store)
48
+ fullchain_cert = self.cert_issuer.sign_csr(csr, challenge_solver=selected_challenge_solver)
49
49
  if fullchain_cert:
50
50
  # Assuming the private key associated with the CSR is not managed by CertManager directly
51
51
  # and is handled by the caller or the cert_issuer's internal process.
@@ -81,17 +81,11 @@ class AcmeCertManager:
81
81
  if len(missing) > 0:
82
82
  issued_certs_list = []
83
83
  # Group missing hosts by the challenge store that supports them
84
- domains_by_store: Dict[ChallengeStore, List[str]] = {}
84
+ domains_by_store: Dict[ChallengeSolver, List[str]] = {}
85
85
  for host in missing:
86
86
  found_store = None
87
- for store in self.challenge_stores:
88
- print(
89
- f"Debug: Checking store {store} for domain {host}. supports_domain: {store.supports_domain(host)}"
90
- )
87
+ for store in self.challenge_solvers:
91
88
  if store.supports_domain(host):
92
- print(
93
- f"Supports domain: '{host}'",
94
- )
95
89
  found_store = store
96
90
  break
97
91
  if found_store is not None:
@@ -99,10 +93,7 @@ class AcmeCertManager:
99
93
  domains_by_store[found_store] = []
100
94
  domains_by_store[found_store].append(host)
101
95
  else:
102
- print(f"Debug: Current challenge_stores: {self.challenge_stores}")
103
- print(f"Warning: No challenge store found that supports domain: {host}. Skipping.")
104
-
105
- # original_challenge_store = self.cert_issuer.challenge_store # Store original
96
+ print(f"Warning: No challenge solver found that supports domain: {host}. Skipping.")
106
97
 
107
98
  for store, domains_to_issue in domains_by_store.items():
108
99
 
@@ -115,7 +106,7 @@ class AcmeCertManager:
115
106
  locality=locality,
116
107
  organization=organization,
117
108
  user_id=user_id,
118
- challenge_store=store,
109
+ challenge_solver=store,
119
110
  )
120
111
 
121
112
  if fullchain_cert:
@@ -125,7 +116,7 @@ class AcmeCertManager:
125
116
  else:
126
117
  print(f"Failed to issue certificate for domains: {domains_to_issue}")
127
118
 
128
- # self.cert_issuer.challenge_store = original_challenge_store # Restore original
119
+ # self.cert_issuer.challenge_solver = original_challenge_solver # Restore original
129
120
  return createExistingResponse(existing, issued_certs_list)
130
121
 
131
122
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: certapi
3
- Version: 0.4.2
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")
@@ -16,17 +16,17 @@ src/certapi/acme/Challenge.py
16
16
  src/certapi/acme/Order.py
17
17
  src/certapi/acme/__init__.py
18
18
  src/certapi/acme/http.py
19
- src/certapi/challenge_store/ChallengeStore.py
20
- src/certapi/challenge_store/FileSystemChallengeStore.py
21
- src/certapi/challenge_store/InmemoryChallengeStore.py
22
- src/certapi/challenge_store/__init__.py
23
- src/certapi/challenge_store/dns/__init__.py
24
- src/certapi/challenge_store/dns/cloudflare/__init__.py
25
- src/certapi/challenge_store/dns/cloudflare/cloudflare_challenge_store.py
26
- src/certapi/challenge_store/dns/cloudflare/cloudflare_client.py
27
- src/certapi/challenge_store/dns/digitalocean/__init__.py
28
- src/certapi/challenge_store/dns/digitalocean/digitalocean_challenge_store.py
29
- src/certapi/challenge_store/dns/digitalocean/digitalocean_client.py
19
+ src/certapi/challenge_solver/ChallengeSolver.py
20
+ src/certapi/challenge_solver/FileSystemChallengeSolver.py
21
+ src/certapi/challenge_solver/InmemoryChallengeSolver.py
22
+ src/certapi/challenge_solver/__init__.py
23
+ src/certapi/challenge_solver/dns/__init__.py
24
+ src/certapi/challenge_solver/dns/cloudflare/__init__.py
25
+ src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py
26
+ src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py
27
+ src/certapi/challenge_solver/dns/digitalocean/__init__.py
28
+ src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py
29
+ src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py
30
30
  src/certapi/client/__init__.py
31
31
  src/certapi/client/cert_manager_client.py
32
32
  src/certapi/crypto/__init__.py
@@ -1,24 +0,0 @@
1
- from .ChallengeStore import ChallengeStore
2
- from .InmemoryChallengeStore import InMemoryChallengeStore
3
- import os
4
- from .FileSystemChallengeStore import FileSystemChallengeStore
5
- from .dns import CloudflareChallengeStore, DigitalOceanChallengeStore
6
-
7
-
8
- def get_challenge_store():
9
- """
10
- Factory function to determine the type of store based on environment variables.
11
-
12
- Environment Variables:
13
- - `CHALLENGE_STORE_TYPE`: Can be "memory" or "filesystem".
14
- - `CHALLENGE_STORE_DIR`: Directory for filesystem-based store. Defaults to "./challenges".
15
- """
16
- store_type = os.getenv("CHALLENGE_STORE_TYPE", "filesystem").lower()
17
- directory = os.getenv("CHALLENGE_STORE_DIR", "./challenges")
18
-
19
- if store_type == "memory":
20
- return InMemoryChallengeStore()
21
- elif store_type == "filesystem":
22
- return FileSystemChallengeStore(directory)
23
- else:
24
- raise ValueError(f"Unknown CHALLENGE_STORE_TYPE: {store_type}")
@@ -1,2 +0,0 @@
1
- from .digitalocean.digitalocean_challenge_store import DigitalOceanChallengeStore
2
- from .cloudflare.cloudflare_challenge_store import CloudflareChallengeStore
File without changes
File without changes
File without changes
File without changes
File without changes