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.
- {certapi-0.4.2 → certapi-0.4.3}/PKG-INFO +4 -4
- {certapi-0.4.2 → certapi-0.4.3}/README.md +3 -3
- {certapi-0.4.2 → certapi-0.4.3}/setup.py +1 -1
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/__init__.py +6 -6
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/Acme.py +9 -6
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/AcmeError.py +3 -1
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/Challenge.py +8 -6
- certapi-0.4.2/src/certapi/challenge_store/ChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/ChallengeSolver.py +5 -5
- certapi-0.4.2/src/certapi/challenge_store/FileSystemChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/FileSystemChallengeSolver.py +8 -3
- certapi-0.4.2/src/certapi/challenge_store/InmemoryChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/InmemoryChallengeSolver.py +6 -2
- certapi-0.4.3/src/certapi/challenge_solver/__init__.py +24 -0
- certapi-0.4.3/src/certapi/challenge_solver/dns/__init__.py +2 -0
- 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
- {certapi-0.4.2/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/cloudflare/cloudflare_client.py +5 -4
- 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
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/crypto/crypto_classes.py +36 -8
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/issuers/AcmeCertIssuer.py +37 -30
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/issuers/abstract_certissuer.py +17 -16
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/FileSystemKeystore.py +4 -1
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/KeyStore.py +3 -3
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/manager/acme_cert_manager.py +14 -23
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi.egg-info/PKG-INFO +4 -4
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi.egg-info/SOURCES.txt +11 -11
- certapi-0.4.2/src/certapi/challenge_store/__init__.py +0 -24
- certapi-0.4.2/src/certapi/challenge_store/dns/__init__.py +0 -2
- {certapi-0.4.2 → certapi-0.4.3}/MANIFEST.in +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/pyproject.toml +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/setup.cfg +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/Order.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/__init__.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/acme/http.py +0 -0
- {certapi-0.4.2/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/cloudflare/__init__.py +0 -0
- {certapi-0.4.2/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/digitalocean/__init__.py +0 -0
- {certapi-0.4.2/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/digitalocean/digitalocean_client.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/client/__init__.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/client/cert_manager_client.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/crypto/__init__.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/crypto/crypto.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/http/__init__.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/http/client.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/http/types.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/issuers/SelfCertIssuer.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/issuers/__init__.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/RemoteKeyStore.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/SqliteKeyStore.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/keystore/__init__.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/manager/__init__.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/server/__init__.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/server/api.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/server/cert_api.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/server/key_api.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/util.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi/utils.py +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-0.4.2 → certapi-0.4.3}/src/certapi.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
33
|
+
from certapi import FileSystemChallengeSolver, FilesystemKeyStore, CertAuthority
|
|
34
34
|
|
|
35
35
|
key_store = FilesystemKeyStore("data")
|
|
36
|
-
|
|
36
|
+
challenge_solver = FileSystemChallengeSolver("./acme-challenges") # this should be where your web server hosts the .well-known/
|
|
37
37
|
|
|
38
|
-
certAuthority = CertAuthority(
|
|
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
|
|
20
|
+
from certapi import FileSystemChallengeSolver, FilesystemKeyStore, CertAuthority
|
|
21
21
|
|
|
22
22
|
key_store = FilesystemKeyStore("data")
|
|
23
|
-
|
|
23
|
+
challenge_solver = FileSystemChallengeSolver("./acme-challenges") # this should be where your web server hosts the .well-known/
|
|
24
24
|
|
|
25
|
-
certAuthority = CertAuthority(
|
|
25
|
+
certAuthority = CertAuthority(challenge_solver, key_store)
|
|
26
26
|
certAuthority.setup()
|
|
27
27
|
|
|
28
28
|
(response,_) = certAuthority.obtainCert("example.com")
|
|
@@ -13,11 +13,11 @@ from .crypto import (
|
|
|
13
13
|
EllipticCurvePrivateKey,
|
|
14
14
|
)
|
|
15
15
|
from .keystore import FileSystemKeystore, SqliteKeyStore, PostgresKeyStore, KeyStore
|
|
16
|
-
from .
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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:
|
|
26
|
-
self.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(
|
|
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":
|
|
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(
|
|
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
|
|
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(
|
|
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="
|
|
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, "
|
|
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:{
|
|
86
|
+
f"'{type}' not found in challenges for {self.domain}, available: {','.join(ch_types)}",
|
|
85
87
|
{"response": self._data["challenges"]},
|
|
86
|
-
"
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from .ChallengeSolver import ChallengeSolver
|
|
2
3
|
import os
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
class
|
|
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
|
|
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
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from .ChallengeSolver import ChallengeSolver
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
class
|
|
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}")
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
3
|
from collections.abc import MutableMapping
|
|
4
|
-
from
|
|
4
|
+
from typing import Literal
|
|
5
|
+
from ...ChallengeSolver import ChallengeSolver
|
|
5
6
|
from .cloudflare_client import Cloudflare
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
class
|
|
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:
|
|
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
|
-
|
|
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
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from ...ChallengeSolver import ChallengeSolver
|
|
4
5
|
from .digitalocean_client import DigitalOcean
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
236
|
+
return self._jwk
|
|
226
237
|
|
|
227
238
|
def algorithm_name(self):
|
|
228
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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,
|
|
33
|
+
self, csr: x509.CertificateSigningRequest, challenge_solver: ChallengeSolver = None, expiry_days: int = 90
|
|
34
34
|
) -> str:
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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],
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
130
|
-
) ->
|
|
145
|
+
challenge_solver: Optional[ChallengeSolver] = None,
|
|
146
|
+
) -> Tuple[Key, str]:
|
|
131
147
|
"""Create a new certificate with a generated key."""
|
|
132
|
-
|
|
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,
|
|
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 ..
|
|
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
|
-
|
|
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],
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
85
|
+
challenge_solver: Optional[ChallengeSolver] = None,
|
|
77
86
|
) -> tuple:
|
|
78
87
|
"""Create a new certificate with a generated key."""
|
|
79
|
-
|
|
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
|
-
|
|
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(
|
|
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 ..
|
|
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
|
-
|
|
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.
|
|
25
|
-
print(f"AcmeCertManager initialized with
|
|
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
|
-
|
|
40
|
-
for store in self.
|
|
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
|
-
|
|
42
|
+
selected_challenge_solver = store
|
|
43
43
|
break
|
|
44
44
|
|
|
45
|
-
if
|
|
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,
|
|
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[
|
|
84
|
+
domains_by_store: Dict[ChallengeSolver, List[str]] = {}
|
|
85
85
|
for host in missing:
|
|
86
86
|
found_store = None
|
|
87
|
-
for store in self.
|
|
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"
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
33
|
+
from certapi import FileSystemChallengeSolver, FilesystemKeyStore, CertAuthority
|
|
34
34
|
|
|
35
35
|
key_store = FilesystemKeyStore("data")
|
|
36
|
-
|
|
36
|
+
challenge_solver = FileSystemChallengeSolver("./acme-challenges") # this should be where your web server hosts the .well-known/
|
|
37
37
|
|
|
38
|
-
certAuthority = CertAuthority(
|
|
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/
|
|
20
|
-
src/certapi/
|
|
21
|
-
src/certapi/
|
|
22
|
-
src/certapi/
|
|
23
|
-
src/certapi/
|
|
24
|
-
src/certapi/
|
|
25
|
-
src/certapi/
|
|
26
|
-
src/certapi/
|
|
27
|
-
src/certapi/
|
|
28
|
-
src/certapi/
|
|
29
|
-
src/certapi/
|
|
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}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|