certapi 0.6.0__tar.gz → 1.0.1__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.6.0/src/certapi.egg-info → certapi-1.0.1}/PKG-INFO +47 -6
- certapi-1.0.1/README.md +93 -0
- {certapi-0.6.0 → certapi-1.0.1}/setup.py +1 -1
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/__init__.py +1 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/AcmeError.py +5 -7
- certapi-1.0.1/src/certapi/client/__init__.py +1 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/http/HttpClientBase.py +1 -1
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/issuers/AcmeCertIssuer.py +10 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/manager/acme_cert_manager.py +11 -3
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/server/api.py +62 -11
- {certapi-0.6.0 → certapi-1.0.1/src/certapi.egg-info}/PKG-INFO +47 -6
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi.egg-info/SOURCES.txt +1 -0
- certapi-1.0.1/tests/test_acme_error_handling.py +704 -0
- {certapi-0.6.0 → certapi-1.0.1}/tests/test_http_error_handling.py +33 -43
- {certapi-0.6.0 → certapi-1.0.1}/tests/test_keystores.py +1 -1
- certapi-0.6.0/README.md +0 -52
- certapi-0.6.0/src/certapi/server/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/MANIFEST.in +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/pyproject.toml +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/setup.cfg +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/Acme.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/Challenge.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/Order.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/http.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/client/cert_manager_client.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/crypto/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/crypto/crypto.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/crypto/crypto_classes.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/errors.py +0 -0
- {certapi-0.6.0/src/certapi/client → certapi-1.0.1/src/certapi/http}/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/http/types.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/issuers/SelfCertIssuer.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/issuers/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/issuers/abstract_certissuer.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/KeyStore.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/RemoteKeyStore.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/SqliteKeyStore.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/__init__.py +0 -0
- {certapi-0.6.0/src/certapi/http → certapi-1.0.1/src/certapi/manager}/__init__.py +0 -0
- {certapi-0.6.0/src/certapi/manager → certapi-1.0.1/src/certapi/server}/__init__.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/server/cert_api.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/server/key_api.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/util.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi/utils.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi.egg-info/requires.txt +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/src/certapi.egg-info/top_level.txt +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/tests/test_cert_issuer_generic.py +0 -0
- {certapi-0.6.0 → certapi-1.0.1}/tests/test_certs_with_key_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: certapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
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
|
|
@@ -27,7 +27,10 @@ Dynamic: summary
|
|
|
27
27
|
CertApi is a Python package for requesting SSL certificates from ACME.
|
|
28
28
|
This is to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
[](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
|
|
31
|
+
[](https://codecov.io/github/mesudip/certapi)
|
|
32
|
+
[](https://pypi.org/project/certapi/)
|
|
33
|
+
|
|
31
34
|
|
|
32
35
|
For a detailed list of changes, please refer to the [CHANGELOG.md](CHANGELOG.md).
|
|
33
36
|
|
|
@@ -39,7 +42,7 @@ You can install CertApi using pip
|
|
|
39
42
|
pip install certapi
|
|
40
43
|
```
|
|
41
44
|
|
|
42
|
-
## Example:
|
|
45
|
+
## Example: Low Leve API : Certificate with Cloudflare
|
|
43
46
|
|
|
44
47
|
```python
|
|
45
48
|
import json
|
|
@@ -51,7 +54,7 @@ from certapi import CertApiException, CloudflareChallengeSolver, Key, AcmeCertIs
|
|
|
51
54
|
challenge_solver = CloudflareChallengeSolver(api_key=None)
|
|
52
55
|
|
|
53
56
|
## initialize cert issuer with a new account key
|
|
54
|
-
cert_issuer = AcmeCertIssuer(Key.generate('
|
|
57
|
+
cert_issuer = AcmeCertIssuer(Key.generate('ecdsa'), challenge_solver)
|
|
55
58
|
|
|
56
59
|
# Preform setup i.e. fetching directory and registering ACME account
|
|
57
60
|
cert_issuer.setup()
|
|
@@ -70,7 +73,45 @@ except CertApiException as e:
|
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
|
|
73
|
-
## Example:
|
|
76
|
+
## Example: High Level API (with AcmeCertManager)
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
The `AcmeCertManager` provides a high-level interface that handles certificate storage, automatic renewal checks, and multi-solver management.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from certapi import (
|
|
82
|
+
AcmeCertManager,
|
|
83
|
+
FileSystemKeyStore,
|
|
84
|
+
AcmeCertIssuer,
|
|
85
|
+
CloudflareChallengeSolver
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# 1. Setup KeyStore to persist keys and certificates
|
|
89
|
+
key_store = FileSystemKeyStore("db")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# DNS-01 via Cloudflare (e.g. for wildcard certs or internal domains)
|
|
93
|
+
dns_solver = CloudflareChallengeSolver(api_token="your-cloudflare-token")
|
|
94
|
+
|
|
95
|
+
# 3. Initialize and Setup AcmeCertManager
|
|
96
|
+
# Create cert issuer with the default challenge solver
|
|
97
|
+
cert_issuer = AcmeCertIssuer.with_keystore(key_store, dns_solver)
|
|
98
|
+
|
|
99
|
+
cert_manager = AcmeCertManager(
|
|
100
|
+
key_store=key_store,
|
|
101
|
+
cert_issuer=cert_issuer,
|
|
102
|
+
challenge_solvers=[dns_solver], # other solvers can be used
|
|
103
|
+
renew_threshold_days=7
|
|
104
|
+
)
|
|
105
|
+
cert_manager.setup()
|
|
106
|
+
|
|
107
|
+
# 4. Issue or Reuse Certificate
|
|
108
|
+
# Automatically checks keystore and renews only if necessary
|
|
109
|
+
response = cert_manager.issue_certificate(["example.com", "www.example.com"])
|
|
110
|
+
|
|
111
|
+
for cert_data in response.issued:
|
|
112
|
+
print(f"Newly issued for: {cert_data.domains}")
|
|
113
|
+
print(cert_data.cert)
|
|
114
|
+
|
|
115
|
+
for cert_data in response.existing:
|
|
116
|
+
print(f"Reusing existing for: {cert_data.domains}")
|
|
76
117
|
```
|
certapi-1.0.1/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# CertApi
|
|
2
|
+
|
|
3
|
+
CertApi is a Python package for requesting SSL certificates from ACME.
|
|
4
|
+
This is to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
|
|
5
|
+
|
|
6
|
+
[](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
|
|
7
|
+
[](https://codecov.io/github/mesudip/certapi)
|
|
8
|
+
[](https://pypi.org/project/certapi/)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
For a detailed list of changes, please refer to the [CHANGELOG.md](CHANGELOG.md).
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
You can install CertApi using pip
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install certapi
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Example: Low Leve API : Certificate with Cloudflare
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import json
|
|
25
|
+
from certapi import CertApiException, CloudflareChallengeSolver, Key, AcmeCertIssuer
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Initialize the Cloudflare challenge solver
|
|
29
|
+
# The API key is read from the CLOUDFLARE_API_KEY environment variable, or you can set it below.
|
|
30
|
+
challenge_solver = CloudflareChallengeSolver(api_key=None)
|
|
31
|
+
|
|
32
|
+
## initialize cert issuer with a new account key
|
|
33
|
+
cert_issuer = AcmeCertIssuer(Key.generate('ecdsa'), challenge_solver)
|
|
34
|
+
|
|
35
|
+
# Preform setup i.e. fetching directory and registering ACME account
|
|
36
|
+
cert_issuer.setup()
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Obtain a certificate for your domain
|
|
40
|
+
(key, cert) = cert_issuer.generate_key_and_cert_for_domain("your-domain.com")
|
|
41
|
+
|
|
42
|
+
print("------ Private Key -----")
|
|
43
|
+
print(key.to_pem())
|
|
44
|
+
print("------- Certificate ------")
|
|
45
|
+
print(cert)
|
|
46
|
+
except CertApiException as e:
|
|
47
|
+
print(f"An error occurred:", json.dumps(e.json_obj(), indent=2))
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Example: High Level API (with AcmeCertManager)
|
|
53
|
+
|
|
54
|
+
The `AcmeCertManager` provides a high-level interface that handles certificate storage, automatic renewal checks, and multi-solver management.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from certapi import (
|
|
58
|
+
AcmeCertManager,
|
|
59
|
+
FileSystemKeyStore,
|
|
60
|
+
AcmeCertIssuer,
|
|
61
|
+
CloudflareChallengeSolver
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 1. Setup KeyStore to persist keys and certificates
|
|
65
|
+
key_store = FileSystemKeyStore("db")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# DNS-01 via Cloudflare (e.g. for wildcard certs or internal domains)
|
|
69
|
+
dns_solver = CloudflareChallengeSolver(api_token="your-cloudflare-token")
|
|
70
|
+
|
|
71
|
+
# 3. Initialize and Setup AcmeCertManager
|
|
72
|
+
# Create cert issuer with the default challenge solver
|
|
73
|
+
cert_issuer = AcmeCertIssuer.with_keystore(key_store, dns_solver)
|
|
74
|
+
|
|
75
|
+
cert_manager = AcmeCertManager(
|
|
76
|
+
key_store=key_store,
|
|
77
|
+
cert_issuer=cert_issuer,
|
|
78
|
+
challenge_solvers=[dns_solver], # other solvers can be used
|
|
79
|
+
renew_threshold_days=7
|
|
80
|
+
)
|
|
81
|
+
cert_manager.setup()
|
|
82
|
+
|
|
83
|
+
# 4. Issue or Reuse Certificate
|
|
84
|
+
# Automatically checks keystore and renews only if necessary
|
|
85
|
+
response = cert_manager.issue_certificate(["example.com", "www.example.com"])
|
|
86
|
+
|
|
87
|
+
for cert_data in response.issued:
|
|
88
|
+
print(f"Newly issued for: {cert_data.domains}")
|
|
89
|
+
print(cert_data.cert)
|
|
90
|
+
|
|
91
|
+
for cert_data in response.existing:
|
|
92
|
+
print(f"Reusing existing for: {cert_data.domains}")
|
|
93
|
+
```
|
|
@@ -28,16 +28,17 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
|
|
|
28
28
|
|
|
29
29
|
def __init__(self, response: requests.Response, step: str):
|
|
30
30
|
requests.HTTPError.__init__(self, response=response)
|
|
31
|
-
self.response = response
|
|
32
31
|
(message, detail) = self.extract_acme_response_error()
|
|
33
32
|
AcmeError.__init__(self, message, detail, step)
|
|
33
|
+
self.response = response
|
|
34
|
+
|
|
34
35
|
|
|
35
36
|
def extract_acme_response_error(self):
|
|
36
37
|
message = None
|
|
37
38
|
error = None
|
|
38
39
|
try:
|
|
39
40
|
res_json = self.response.json()
|
|
40
|
-
if res_json
|
|
41
|
+
if res_json.get("status") == "invalid":
|
|
41
42
|
if res_json["challenges"]:
|
|
42
43
|
failed_challenge: dict = [x for x in res_json["challenges"] if x["status"] == "invalid"][0]
|
|
43
44
|
error = failed_challenge["error"]
|
|
@@ -108,10 +109,7 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
|
|
|
108
109
|
hostname = error.get("hostname", "unknown")
|
|
109
110
|
status = error.get("status", "unknown")
|
|
110
111
|
message = (
|
|
111
|
-
str(hostname)
|
|
112
|
-
+ " Status="
|
|
113
|
-
+ str(status)
|
|
114
|
-
+ ": Invalid response in challenge url"
|
|
112
|
+
str(hostname) + " Status=" + str(status) + ": Invalid response in challenge url"
|
|
115
113
|
)
|
|
116
114
|
else:
|
|
117
115
|
message = err_detail
|
|
@@ -143,4 +141,4 @@ class AcmeInvalidNonceError(AcmeHttpError):
|
|
|
143
141
|
def __init__(self, response: requests.Response, step: str):
|
|
144
142
|
super().__init__(response, step)
|
|
145
143
|
self.can_retry = True
|
|
146
|
-
self.response=0
|
|
144
|
+
self.response = 0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .cert_manager_client import CertManagerClient
|
|
@@ -80,7 +80,7 @@ class HttpClientBase(object):
|
|
|
80
80
|
return self._req(method, url, step, json_data, data, params, timeout)
|
|
81
81
|
except CertApiException as e:
|
|
82
82
|
if e.can_retry and i < retries:
|
|
83
|
-
retry_delay = e.retry_delay if hasattr(e,
|
|
83
|
+
retry_delay = e.retry_delay if hasattr(e, "retry_delay") else delay
|
|
84
84
|
print(f"Retrying {method} {url} in {retry_delay} seconds due to retryable error: {e.message}")
|
|
85
85
|
time.sleep(retry_delay)
|
|
86
86
|
else:
|
|
@@ -21,6 +21,16 @@ class AcmeCertIssuer(CertIssuer):
|
|
|
21
21
|
self.challenge_solver = challenge_solver
|
|
22
22
|
self.self_verify_challenge = self_verify_challenge
|
|
23
23
|
|
|
24
|
+
@staticmethod
|
|
25
|
+
def with_keystore(
|
|
26
|
+
key_store: "KeyStore",
|
|
27
|
+
challenge_solver: ChallengeSolver,
|
|
28
|
+
account_key_name: str = "acme_account.key",
|
|
29
|
+
acme_url: str = None,
|
|
30
|
+
) -> "AcmeCertIssuer":
|
|
31
|
+
account_key, _ = key_store._get_or_generate_key(account_key_name)
|
|
32
|
+
return AcmeCertIssuer(account_key, challenge_solver, acme_url=acme_url)
|
|
33
|
+
|
|
24
34
|
def setup(self):
|
|
25
35
|
self.acme.setup()
|
|
26
36
|
res: Response = self.acme.register()
|
|
@@ -12,6 +12,8 @@ from ..keystore.KeyStore import KeyStore
|
|
|
12
12
|
from cryptography.x509 import Certificate, CertificateSigningRequest
|
|
13
13
|
from ..crypto import Key, certs_to_pem, cert_to_pem, get_csr_hostnames
|
|
14
14
|
|
|
15
|
+
DEFAULT_RENEW_THRESHOLD_DAYS = 62
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
class AcmeCertManager:
|
|
17
19
|
def __init__(
|
|
@@ -19,10 +21,14 @@ class AcmeCertManager:
|
|
|
19
21
|
key_store: KeyStore,
|
|
20
22
|
cert_issuer: AcmeCertIssuer,
|
|
21
23
|
challenge_solvers: List[ChallengeSolver] = [],
|
|
24
|
+
renew_threshold_days: int = DEFAULT_RENEW_THRESHOLD_DAYS, # Renewal will be accepted if cert is valid for less than 75 days
|
|
22
25
|
):
|
|
23
26
|
self.key_store: KeyStore = key_store
|
|
24
27
|
self.cert_issuer: AcmeCertIssuer = cert_issuer
|
|
25
28
|
self.challenge_solvers: List[ChallengeSolver] = challenge_solvers
|
|
29
|
+
self.renew_threshold_days: int = (
|
|
30
|
+
DEFAULT_RENEW_THRESHOLD_DAYS if renew_threshold_days is None else renew_threshold_days
|
|
31
|
+
)
|
|
26
32
|
|
|
27
33
|
def setup(self):
|
|
28
34
|
names = [solver.__class__.__name__.replace("ChallengeSolver", "") for solver in self.challenge_solvers]
|
|
@@ -67,6 +73,7 @@ class AcmeCertManager:
|
|
|
67
73
|
locality: Optional[str] = None,
|
|
68
74
|
organization: Optional[str] = None,
|
|
69
75
|
user_id: Optional[str] = None,
|
|
76
|
+
renew_threshold_days: Optional[int] = None,
|
|
70
77
|
) -> CertificateResponse:
|
|
71
78
|
|
|
72
79
|
if type(hosts) == str:
|
|
@@ -77,10 +84,11 @@ class AcmeCertManager:
|
|
|
77
84
|
result = self.key_store.find_key_and_cert_by_domain(h)
|
|
78
85
|
if result is not None:
|
|
79
86
|
# result is (domain_id, key, cert_list)
|
|
80
|
-
cert=result[2][0]
|
|
87
|
+
cert = result[2][0]
|
|
81
88
|
invalid_date = cert.not_valid_after_utc
|
|
82
|
-
# Check if the certificate is still valid for at least
|
|
83
|
-
if
|
|
89
|
+
# Check if the certificate is still valid for at least renew_threshold_days
|
|
90
|
+
threshold = renew_threshold_days if renew_threshold_days is not None else self.renew_threshold_days
|
|
91
|
+
if invalid_date > datetime.now(timezone.utc) + timedelta(days=threshold):
|
|
84
92
|
existing[h] = result
|
|
85
93
|
missing = [h for h in hosts if h not in existing]
|
|
86
94
|
if len(missing) > 0:
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import threading
|
|
3
|
+
import os
|
|
2
4
|
from flask import request, jsonify
|
|
3
5
|
from cryptography.x509 import CertificateSigningRequest
|
|
4
6
|
from flask_restx import Resource, reqparse, fields
|
|
@@ -6,7 +8,48 @@ from flask_restx import Resource, reqparse, fields
|
|
|
6
8
|
from certapi import AcmeCertManager
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
class RenewalQueueFullError(Exception):
|
|
12
|
+
"""Exception raised when the renewal queue is full."""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RenewalLockManager:
|
|
18
|
+
def __init__(self, queue_size: int = 5):
|
|
19
|
+
self.queue_size = queue_size
|
|
20
|
+
self._lock = threading.Lock()
|
|
21
|
+
self._condition = threading.Condition(self._lock)
|
|
22
|
+
self._renewing_domains = set()
|
|
23
|
+
self._waiting_threads = 0
|
|
24
|
+
|
|
25
|
+
def acquire(self, domains):
|
|
26
|
+
while True:
|
|
27
|
+
with self._lock:
|
|
28
|
+
# Check if any domain is already being renewed by another thread
|
|
29
|
+
if any(h in self._renewing_domains for h in domains):
|
|
30
|
+
if self._waiting_threads >= self.queue_size:
|
|
31
|
+
raise RenewalQueueFullError("Proper queue is full")
|
|
32
|
+
|
|
33
|
+
self._waiting_threads += 1
|
|
34
|
+
self._condition.wait()
|
|
35
|
+
self._waiting_threads -= 1
|
|
36
|
+
continue # Re-check everything after waking up
|
|
37
|
+
|
|
38
|
+
# Mark as renewing
|
|
39
|
+
for h in domains:
|
|
40
|
+
self._renewing_domains.add(h)
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
def release(self, domains):
|
|
44
|
+
with self._lock:
|
|
45
|
+
for h in domains:
|
|
46
|
+
self._renewing_domains.discard(h)
|
|
47
|
+
self._condition.notify_all()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_api_resources(api_ns, cert_manager: AcmeCertManager, renew_queue_size: int = 5):
|
|
51
|
+
|
|
52
|
+
lock_manager = RenewalLockManager(queue_size=renew_queue_size)
|
|
10
53
|
|
|
11
54
|
# Models for documentation
|
|
12
55
|
issued_cert_model = api_ns.model(
|
|
@@ -45,6 +88,7 @@ def create_api_resources(api_ns, cert_manager: AcmeCertManager):
|
|
|
45
88
|
obtain_parser.add_argument("locality", type=str, help="Locality name")
|
|
46
89
|
obtain_parser.add_argument("organization", type=str, help="Organization name")
|
|
47
90
|
obtain_parser.add_argument("user_id", type=str, help="User ID")
|
|
91
|
+
obtain_parser.add_argument("renew_threshold_days", type=int, help="Threshold in days for certificate reuse")
|
|
48
92
|
|
|
49
93
|
@api_ns.route("/obtain")
|
|
50
94
|
class ObtainCert(Resource):
|
|
@@ -59,16 +103,23 @@ def create_api_resources(api_ns, cert_manager: AcmeCertManager):
|
|
|
59
103
|
args = obtain_parser.parse_args()
|
|
60
104
|
hostnames = args["hostname"]
|
|
61
105
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
106
|
+
# Acquire lock for the domains
|
|
107
|
+
lock_manager.acquire(hostnames)
|
|
108
|
+
try:
|
|
109
|
+
data = cert_manager.issue_certificate(
|
|
110
|
+
hostnames,
|
|
111
|
+
key_type=args["key_type"],
|
|
112
|
+
expiry_days=args["expiry_days"],
|
|
113
|
+
country=args["country"],
|
|
114
|
+
state=args["state"],
|
|
115
|
+
locality=args["locality"],
|
|
116
|
+
organization=args["organization"],
|
|
117
|
+
user_id=args["user_id"],
|
|
118
|
+
renew_threshold_days=args.get("renew_threshold_days"),
|
|
119
|
+
)
|
|
120
|
+
finally:
|
|
121
|
+
lock_manager.release(hostnames)
|
|
122
|
+
|
|
72
123
|
print(data)
|
|
73
124
|
print(data.to_json())
|
|
74
125
|
if data:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: certapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
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
|
|
@@ -27,7 +27,10 @@ Dynamic: summary
|
|
|
27
27
|
CertApi is a Python package for requesting SSL certificates from ACME.
|
|
28
28
|
This is to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
[](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
|
|
31
|
+
[](https://codecov.io/github/mesudip/certapi)
|
|
32
|
+
[](https://pypi.org/project/certapi/)
|
|
33
|
+
|
|
31
34
|
|
|
32
35
|
For a detailed list of changes, please refer to the [CHANGELOG.md](CHANGELOG.md).
|
|
33
36
|
|
|
@@ -39,7 +42,7 @@ You can install CertApi using pip
|
|
|
39
42
|
pip install certapi
|
|
40
43
|
```
|
|
41
44
|
|
|
42
|
-
## Example:
|
|
45
|
+
## Example: Low Leve API : Certificate with Cloudflare
|
|
43
46
|
|
|
44
47
|
```python
|
|
45
48
|
import json
|
|
@@ -51,7 +54,7 @@ from certapi import CertApiException, CloudflareChallengeSolver, Key, AcmeCertIs
|
|
|
51
54
|
challenge_solver = CloudflareChallengeSolver(api_key=None)
|
|
52
55
|
|
|
53
56
|
## initialize cert issuer with a new account key
|
|
54
|
-
cert_issuer = AcmeCertIssuer(Key.generate('
|
|
57
|
+
cert_issuer = AcmeCertIssuer(Key.generate('ecdsa'), challenge_solver)
|
|
55
58
|
|
|
56
59
|
# Preform setup i.e. fetching directory and registering ACME account
|
|
57
60
|
cert_issuer.setup()
|
|
@@ -70,7 +73,45 @@ except CertApiException as e:
|
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
|
|
73
|
-
## Example:
|
|
76
|
+
## Example: High Level API (with AcmeCertManager)
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
The `AcmeCertManager` provides a high-level interface that handles certificate storage, automatic renewal checks, and multi-solver management.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from certapi import (
|
|
82
|
+
AcmeCertManager,
|
|
83
|
+
FileSystemKeyStore,
|
|
84
|
+
AcmeCertIssuer,
|
|
85
|
+
CloudflareChallengeSolver
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# 1. Setup KeyStore to persist keys and certificates
|
|
89
|
+
key_store = FileSystemKeyStore("db")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# DNS-01 via Cloudflare (e.g. for wildcard certs or internal domains)
|
|
93
|
+
dns_solver = CloudflareChallengeSolver(api_token="your-cloudflare-token")
|
|
94
|
+
|
|
95
|
+
# 3. Initialize and Setup AcmeCertManager
|
|
96
|
+
# Create cert issuer with the default challenge solver
|
|
97
|
+
cert_issuer = AcmeCertIssuer.with_keystore(key_store, dns_solver)
|
|
98
|
+
|
|
99
|
+
cert_manager = AcmeCertManager(
|
|
100
|
+
key_store=key_store,
|
|
101
|
+
cert_issuer=cert_issuer,
|
|
102
|
+
challenge_solvers=[dns_solver], # other solvers can be used
|
|
103
|
+
renew_threshold_days=7
|
|
104
|
+
)
|
|
105
|
+
cert_manager.setup()
|
|
106
|
+
|
|
107
|
+
# 4. Issue or Reuse Certificate
|
|
108
|
+
# Automatically checks keystore and renews only if necessary
|
|
109
|
+
response = cert_manager.issue_certificate(["example.com", "www.example.com"])
|
|
110
|
+
|
|
111
|
+
for cert_data in response.issued:
|
|
112
|
+
print(f"Newly issued for: {cert_data.domains}")
|
|
113
|
+
print(cert_data.cert)
|
|
114
|
+
|
|
115
|
+
for cert_data in response.existing:
|
|
116
|
+
print(f"Reusing existing for: {cert_data.domains}")
|
|
76
117
|
```
|
|
@@ -52,6 +52,7 @@ src/certapi/server/__init__.py
|
|
|
52
52
|
src/certapi/server/api.py
|
|
53
53
|
src/certapi/server/cert_api.py
|
|
54
54
|
src/certapi/server/key_api.py
|
|
55
|
+
tests/test_acme_error_handling.py
|
|
55
56
|
tests/test_cert_issuer_generic.py
|
|
56
57
|
tests/test_certs_with_key_types.py
|
|
57
58
|
tests/test_http_error_handling.py
|