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.
- {certapi-0.4.1 → certapi-0.4.3}/PKG-INFO +4 -4
- {certapi-0.4.1 → certapi-0.4.3}/README.md +3 -3
- {certapi-0.4.1 → certapi-0.4.3}/setup.py +1 -1
- certapi-0.4.3/src/certapi/__init__.py +23 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/Acme.py +11 -7
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/AcmeError.py +3 -1
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/Challenge.py +17 -18
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/Order.py +3 -2
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/__init__.py +1 -1
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/acme/http.py +1 -2
- certapi-0.4.1/src/certapi/challenge_store/ChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/ChallengeSolver.py +8 -6
- certapi-0.4.1/src/certapi/challenge_store/FileSystemChallengeStore.py → certapi-0.4.3/src/certapi/challenge_solver/FileSystemChallengeSolver.py +9 -4
- certapi-0.4.1/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.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
- {certapi-0.4.1/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/cloudflare/cloudflare_client.py +6 -5
- 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
- {certapi-0.4.1/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/digitalocean/digitalocean_client.py +1 -1
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/client/cert_manager_client.py +29 -11
- certapi-0.4.3/src/certapi/crypto/__init__.py +2 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/crypto/crypto.py +4 -3
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/crypto/crypto_classes.py +40 -12
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/http/types.py +13 -6
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/issuers/AcmeCertIssuer.py +57 -39
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/issuers/SelfCertIssuer.py +3 -2
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/issuers/__init__.py +1 -1
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/issuers/abstract_certissuer.py +35 -26
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/FileSystemKeystore.py +11 -8
- certapi-0.4.3/src/certapi/keystore/KeyStore.py +71 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/PostgresqlKeyStore.py +12 -5
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/RemoteKeyStore.py +7 -3
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/SqliteKeyStore.py +7 -4
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/keystore/__init__.py +1 -1
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/manager/acme_cert_manager.py +41 -44
- certapi-0.4.3/src/certapi/server/api.py +103 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/server/cert_api.py +48 -36
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/server/key_api.py +25 -28
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/utils.py +0 -1
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi.egg-info/PKG-INFO +4 -4
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi.egg-info/SOURCES.txt +11 -12
- certapi-0.4.1/src/certapi/__init__.py +0 -9
- certapi-0.4.1/src/certapi/challenge_store/FilesystemChallengeStore.py +0 -38
- certapi-0.4.1/src/certapi/challenge_store/__init__.py +0 -25
- certapi-0.4.1/src/certapi/challenge_store/dns/__init__.py +0 -2
- certapi-0.4.1/src/certapi/crypto/__init__.py +0 -2
- certapi-0.4.1/src/certapi/keystore/KeyStore.py +0 -66
- certapi-0.4.1/src/certapi/server/api.py +0 -89
- {certapi-0.4.1 → certapi-0.4.3}/MANIFEST.in +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/pyproject.toml +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/setup.cfg +0 -0
- {certapi-0.4.1/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/cloudflare/__init__.py +0 -0
- {certapi-0.4.1/src/certapi/challenge_store → certapi-0.4.3/src/certapi/challenge_solver}/dns/digitalocean/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/client/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/http/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/http/client.py +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/manager/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/server/__init__.py +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi/util.py +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-0.4.1 → certapi-0.4.3}/src/certapi.egg-info/requires.txt +0 -0
- {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.
|
|
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")
|
|
@@ -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
|
|
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:
|
|
25
|
-
self.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(
|
|
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":
|
|
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(
|
|
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
|
|
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 =
|
|
36
|
-
value =
|
|
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="
|
|
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, "
|
|
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:{
|
|
86
|
+
f"'{type}' not found in challenges for {self.domain}, available: {','.join(ch_types)}",
|
|
88
87
|
{"response": self._data["challenges"]},
|
|
89
|
-
"
|
|
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,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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 .
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from .ChallengeSolver import ChallengeSolver
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
-
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:
|
|
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
|
-
|
|
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
|
-
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(
|
|
15
|
-
|
|
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:
|
|
36
|
-
|
|
37
|
-
if
|
|
38
|
-
|
|
39
|
-
if
|
|
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(
|
|
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(
|
|
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:
|
|
98
|
-
|
|
114
|
+
if name:
|
|
115
|
+
data["name"] = name
|
|
116
|
+
return self._post("/certs", data=data)
|
|
@@ -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
|
|
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
|