certapi 1.1.3__tar.gz → 1.1.4__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-1.1.3/src/certapi.egg-info → certapi-1.1.4}/PKG-INFO +1 -1
- {certapi-1.1.3 → certapi-1.1.4}/setup.py +1 -1
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/client/renewal_manager.py +37 -15
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/manager/acme_cert_manager.py +14 -1
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/server/api.py +17 -35
- {certapi-1.1.3 → certapi-1.1.4/src/certapi.egg-info}/PKG-INFO +1 -1
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_acme_cert_manager_batching.py +40 -0
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_obtain_interface.py +15 -5
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_renewal_manager.py +34 -3
- {certapi-1.1.3 → certapi-1.1.4}/MANIFEST.in +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/README.md +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/pyproject.toml +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/setup.cfg +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/Acme.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/AcmeError.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/Challenge.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/Order.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/http.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/cli.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/client/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/client/cert_manager_client.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/crypto/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/crypto/crypto.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/crypto/crypto_classes.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/domain_batching.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/errors.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/http/HttpClientBase.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/http/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/http/types.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/issuers/AcmeCertIssuer.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/issuers/SelfCertIssuer.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/issuers/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/issuers/abstract_certissuer.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/KeyStore.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/RemoteKeyStore.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/SqliteKeyStore.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/manager/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/server/__init__.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/server/cert_api.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/server/key_api.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/util.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi/utils.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/SOURCES.txt +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/entry_points.txt +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/requires.txt +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/top_level.txt +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_acme_error_handling.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_cert_issuer_generic.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_certs_with_key_types.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_cli.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_domain_batching_boulder_cases.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_http_error_handling.py +0 -0
- {certapi-1.1.3 → certapi-1.1.4}/tests/test_keystores.py +0 -0
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
import threading
|
|
3
3
|
import time
|
|
4
4
|
from datetime import datetime, timezone, timedelta
|
|
5
|
-
from typing import Callable, Dict, Optional, Set, Any, List
|
|
5
|
+
from typing import Callable, Dict, Optional, Set, Any, List, Literal
|
|
6
6
|
|
|
7
7
|
from certapi.crypto import Key, certs_from_pem
|
|
8
8
|
from certapi.client.cert_manager_client import CertManagerClient
|
|
@@ -42,9 +42,27 @@ class RenewalManager:
|
|
|
42
42
|
remote_poll_interval_seconds: int = 30,
|
|
43
43
|
clock_fn: Optional[Callable[[], datetime]] = None,
|
|
44
44
|
sleep_fn: Optional[Callable[[float], None]] = None,
|
|
45
|
+
key_type: Literal["rsa", "ecdsa", "ed25519"] = "ecdsa",
|
|
46
|
+
expiry_days: int = 90,
|
|
47
|
+
batch_domains: bool = False,
|
|
48
|
+
self_verify: bool = True,
|
|
49
|
+
country: Optional[str] = None,
|
|
50
|
+
state: Optional[str] = None,
|
|
51
|
+
locality: Optional[str] = None,
|
|
52
|
+
organization: Optional[str] = None,
|
|
53
|
+
user_id: Optional[str] = None,
|
|
45
54
|
):
|
|
46
55
|
self.cert_manager_client = cert_manager_client
|
|
47
56
|
self.sync_watch_domains = sync_watch_domains
|
|
57
|
+
self.key_type = key_type
|
|
58
|
+
self.expiry_days = expiry_days
|
|
59
|
+
self.batch_domains = batch_domains
|
|
60
|
+
self.self_verify = self_verify
|
|
61
|
+
self.country = country
|
|
62
|
+
self.state = state
|
|
63
|
+
self.locality = locality
|
|
64
|
+
self.organization = organization
|
|
65
|
+
self.user_id = user_id
|
|
48
66
|
self.sleep_slack_seconds = sleep_slack_seconds
|
|
49
67
|
self.max_sleep_seconds = max_sleep_seconds
|
|
50
68
|
self.renew_retry_interval_seconds = renew_retry_interval_seconds
|
|
@@ -293,20 +311,24 @@ class RenewalManager:
|
|
|
293
311
|
|
|
294
312
|
try:
|
|
295
313
|
renew_threshold_days = self.cert_min_renew_threshold_secs // (24 * 3600)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
314
|
+
obtain_options = {
|
|
315
|
+
"key_type": self.key_type,
|
|
316
|
+
"expiry_days": self.expiry_days,
|
|
317
|
+
"renew_threshold_days": renew_threshold_days,
|
|
318
|
+
"skip_failing": False,
|
|
319
|
+
"batch_domains": self.batch_domains,
|
|
320
|
+
"self_verify": self.self_verify,
|
|
321
|
+
"country": self.country,
|
|
322
|
+
"state": self.state,
|
|
323
|
+
"locality": self.locality,
|
|
324
|
+
"organization": self.organization,
|
|
325
|
+
"user_id": self.user_id,
|
|
326
|
+
}
|
|
327
|
+
res = self._call_certificate_backend(
|
|
328
|
+
self.cert_manager_client.obtain,
|
|
329
|
+
[domain],
|
|
330
|
+
obtain_options,
|
|
331
|
+
)
|
|
310
332
|
if res is None:
|
|
311
333
|
return
|
|
312
334
|
self._update_expiry_cache(res.issued + res.existing)
|
|
@@ -75,6 +75,8 @@ class AcmeCertManager:
|
|
|
75
75
|
organization: Optional[str] = None,
|
|
76
76
|
user_id: Optional[str] = None,
|
|
77
77
|
renew_threshold_days: Optional[int] = None,
|
|
78
|
+
skip_failing: bool = True,
|
|
79
|
+
batch_domains: bool = False,
|
|
78
80
|
self_verify: bool = True,
|
|
79
81
|
) -> CertificateResponse:
|
|
80
82
|
return self.obtain(
|
|
@@ -87,6 +89,8 @@ class AcmeCertManager:
|
|
|
87
89
|
organization=organization,
|
|
88
90
|
user_id=user_id,
|
|
89
91
|
renew_threshold_days=renew_threshold_days,
|
|
92
|
+
skip_failing=skip_failing,
|
|
93
|
+
batch_domains=batch_domains,
|
|
90
94
|
self_verify=self_verify,
|
|
91
95
|
)
|
|
92
96
|
|
|
@@ -101,6 +105,8 @@ class AcmeCertManager:
|
|
|
101
105
|
organization: Optional[str] = None,
|
|
102
106
|
user_id: Optional[str] = None,
|
|
103
107
|
renew_threshold_days: Optional[int] = None,
|
|
108
|
+
skip_failing: bool = True,
|
|
109
|
+
batch_domains: bool = False,
|
|
104
110
|
self_verify: bool = True,
|
|
105
111
|
) -> CertificateResponse:
|
|
106
112
|
return self._issue_certificate_internal(
|
|
@@ -113,7 +119,8 @@ class AcmeCertManager:
|
|
|
113
119
|
organization=organization,
|
|
114
120
|
user_id=user_id,
|
|
115
121
|
renew_threshold_days=renew_threshold_days,
|
|
116
|
-
batch_generator=None,
|
|
122
|
+
batch_generator=create_safe_domain_batches if batch_domains else None,
|
|
123
|
+
skip_failing=skip_failing,
|
|
117
124
|
self_verify=self_verify,
|
|
118
125
|
)
|
|
119
126
|
|
|
@@ -129,6 +136,7 @@ class AcmeCertManager:
|
|
|
129
136
|
user_id: Optional[str] = None,
|
|
130
137
|
renew_threshold_days: Optional[int] = None,
|
|
131
138
|
batch_generator: Callable[[List[str]], List[List[str]]] = create_safe_domain_batches,
|
|
139
|
+
skip_failing: bool = True,
|
|
132
140
|
self_verify: bool = True,
|
|
133
141
|
) -> CertificateResponse:
|
|
134
142
|
return self._issue_certificate_internal(
|
|
@@ -142,6 +150,7 @@ class AcmeCertManager:
|
|
|
142
150
|
user_id=user_id,
|
|
143
151
|
renew_threshold_days=renew_threshold_days,
|
|
144
152
|
batch_generator=batch_generator,
|
|
153
|
+
skip_failing=skip_failing,
|
|
145
154
|
self_verify=self_verify,
|
|
146
155
|
)
|
|
147
156
|
|
|
@@ -157,6 +166,7 @@ class AcmeCertManager:
|
|
|
157
166
|
user_id: Optional[str] = None,
|
|
158
167
|
renew_threshold_days: Optional[int] = None,
|
|
159
168
|
batch_generator: Optional[Callable[[List[str]], List[List[str]]]] = None,
|
|
169
|
+
skip_failing: bool = True,
|
|
160
170
|
self_verify: bool = True,
|
|
161
171
|
) -> CertificateResponse:
|
|
162
172
|
if isinstance(hosts, str):
|
|
@@ -196,6 +206,9 @@ class AcmeCertManager:
|
|
|
196
206
|
else:
|
|
197
207
|
print(f"Warning: No challenge solver found that supports domain: {host}. Skipping.")
|
|
198
208
|
|
|
209
|
+
if len(domains_by_store) == 0 and not skip_failing:
|
|
210
|
+
raise ValueError("None of the domains are owned by this machine or could be verified")
|
|
211
|
+
|
|
199
212
|
for store, domains_to_issue in domains_by_store.items():
|
|
200
213
|
issuance_batches = (
|
|
201
214
|
batch_generator(domains_to_issue) if batch_generator is not None else [domains_to_issue]
|
|
@@ -122,43 +122,25 @@ def create_api_resources(api_ns, cert_manager: AcmeCertManager, renew_queue_size
|
|
|
122
122
|
def get(self):
|
|
123
123
|
args = obtain_parser.parse_args()
|
|
124
124
|
hostnames = args["hostname"]
|
|
125
|
-
skip_failing = args.get("skip_failing", False)
|
|
126
|
-
self_verify = args.get("self_verify", True)
|
|
127
125
|
|
|
128
126
|
with lock_manager:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
cert_manager.issue_certificate_in_batches
|
|
147
|
-
if args.get("batch_domains")
|
|
148
|
-
else cert_manager.issue_certificate
|
|
149
|
-
)
|
|
150
|
-
data = issue_fn(
|
|
151
|
-
hostnames,
|
|
152
|
-
key_type=args["key_type"],
|
|
153
|
-
expiry_days=args["expiry_days"],
|
|
154
|
-
country=args["country"],
|
|
155
|
-
state=args["state"],
|
|
156
|
-
locality=args["locality"],
|
|
157
|
-
organization=args["organization"],
|
|
158
|
-
user_id=args["user_id"],
|
|
159
|
-
renew_threshold_days=args.get("renew_threshold_days"),
|
|
160
|
-
self_verify=False,
|
|
161
|
-
)
|
|
127
|
+
try:
|
|
128
|
+
data = cert_manager.obtain(
|
|
129
|
+
hostnames,
|
|
130
|
+
key_type=args["key_type"],
|
|
131
|
+
expiry_days=args["expiry_days"],
|
|
132
|
+
country=args["country"],
|
|
133
|
+
state=args["state"],
|
|
134
|
+
locality=args["locality"],
|
|
135
|
+
organization=args["organization"],
|
|
136
|
+
user_id=args["user_id"],
|
|
137
|
+
renew_threshold_days=args.get("renew_threshold_days"),
|
|
138
|
+
skip_failing=args.get("skip_failing", False),
|
|
139
|
+
batch_domains=args.get("batch_domains", False),
|
|
140
|
+
self_verify=args.get("self_verify", True),
|
|
141
|
+
)
|
|
142
|
+
except ValueError as e:
|
|
143
|
+
api_ns.abort(400, message=str(e))
|
|
162
144
|
|
|
163
145
|
print(data)
|
|
164
146
|
if data:
|
|
@@ -98,6 +98,26 @@ def test_issue_certificate_in_batches_uses_custom_batch_generator():
|
|
|
98
98
|
assert [issued.domains for issued in response.issued] == issuer.calls
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def test_obtain_batch_domains_uses_safe_batches():
|
|
102
|
+
key_store = DummyKeyStore()
|
|
103
|
+
issuer = DummyIssuer()
|
|
104
|
+
solver = DummySolver()
|
|
105
|
+
manager = AcmeCertManager(key_store=key_store, cert_issuer=issuer, challenge_solvers=[solver])
|
|
106
|
+
|
|
107
|
+
domains = [
|
|
108
|
+
"x.abc.def.ghi.example.com",
|
|
109
|
+
"root.example.com",
|
|
110
|
+
]
|
|
111
|
+
response = manager.obtain(hosts=domains, batch_domains=True)
|
|
112
|
+
|
|
113
|
+
assert issuer.calls == [
|
|
114
|
+
["root.example.com"],
|
|
115
|
+
["x.abc.def"],
|
|
116
|
+
["ghi.example.com"],
|
|
117
|
+
]
|
|
118
|
+
assert len(response.issued) == 3
|
|
119
|
+
|
|
120
|
+
|
|
101
121
|
def test_issue_certificate_self_verify_false_skips_strict_solver_check():
|
|
102
122
|
class StrictFailingSolver(DummySolver):
|
|
103
123
|
def supports_domain_strict(self, _domain):
|
|
@@ -127,3 +147,23 @@ def test_issue_certificate_self_verify_true_uses_strict_solver_check():
|
|
|
127
147
|
|
|
128
148
|
assert issuer.calls == []
|
|
129
149
|
assert response.issued == []
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_obtain_self_verify_true_raises_when_all_domains_fail_and_skip_failing_false():
|
|
153
|
+
class StrictFailingSolver(DummySolver):
|
|
154
|
+
def supports_domain_strict(self, _domain):
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
key_store = DummyKeyStore()
|
|
158
|
+
issuer = DummyIssuer()
|
|
159
|
+
solver = StrictFailingSolver()
|
|
160
|
+
manager = AcmeCertManager(key_store=key_store, cert_issuer=issuer, challenge_solvers=[solver])
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
manager.obtain(hosts=["force.example.com"], skip_failing=False)
|
|
164
|
+
except ValueError as e:
|
|
165
|
+
assert "None of the domains are owned" in str(e)
|
|
166
|
+
else:
|
|
167
|
+
raise AssertionError("expected strict obtain failure")
|
|
168
|
+
|
|
169
|
+
assert issuer.calls == []
|
|
@@ -4,6 +4,7 @@ from flask import Flask
|
|
|
4
4
|
from flask_restx import Api, Namespace
|
|
5
5
|
|
|
6
6
|
from certapi.client.cert_manager_client import CertManagerClient
|
|
7
|
+
from certapi.domain_batching import create_safe_domain_batches
|
|
7
8
|
from certapi.http.types import CertificateResponse
|
|
8
9
|
from certapi.manager.acme_cert_manager import AcmeCertManager
|
|
9
10
|
from certapi.server.api import create_api_resources
|
|
@@ -29,6 +30,8 @@ def test_acme_cert_manager_issue_certificate_delegates_to_obtain(monkeypatch):
|
|
|
29
30
|
organization="Org",
|
|
30
31
|
user_id="u1",
|
|
31
32
|
renew_threshold_days=25,
|
|
33
|
+
skip_failing=False,
|
|
34
|
+
batch_domains=True,
|
|
32
35
|
self_verify=False,
|
|
33
36
|
)
|
|
34
37
|
|
|
@@ -42,6 +45,8 @@ def test_acme_cert_manager_issue_certificate_delegates_to_obtain(monkeypatch):
|
|
|
42
45
|
assert captured["organization"] == "Org"
|
|
43
46
|
assert captured["user_id"] == "u1"
|
|
44
47
|
assert captured["renew_threshold_days"] == 25
|
|
48
|
+
assert captured["skip_failing"] is False
|
|
49
|
+
assert captured["batch_domains"] is True
|
|
45
50
|
assert captured["self_verify"] is False
|
|
46
51
|
|
|
47
52
|
|
|
@@ -60,6 +65,8 @@ def test_acme_cert_manager_obtain_uses_internal_issue_path(monkeypatch):
|
|
|
60
65
|
key_type="ecdsa",
|
|
61
66
|
expiry_days=90,
|
|
62
67
|
renew_threshold_days=30,
|
|
68
|
+
skip_failing=False,
|
|
69
|
+
batch_domains=True,
|
|
63
70
|
self_verify=False,
|
|
64
71
|
)
|
|
65
72
|
|
|
@@ -68,7 +75,8 @@ def test_acme_cert_manager_obtain_uses_internal_issue_path(monkeypatch):
|
|
|
68
75
|
assert captured["key_type"] == "ecdsa"
|
|
69
76
|
assert captured["expiry_days"] == 90
|
|
70
77
|
assert captured["renew_threshold_days"] == 30
|
|
71
|
-
assert captured["batch_generator"] is
|
|
78
|
+
assert captured["batch_generator"] is create_safe_domain_batches
|
|
79
|
+
assert captured["skip_failing"] is False
|
|
72
80
|
assert captured["self_verify"] is False
|
|
73
81
|
|
|
74
82
|
|
|
@@ -157,7 +165,7 @@ def test_remote_obtain_self_verify_false_skips_server_prefilter():
|
|
|
157
165
|
def __init__(self):
|
|
158
166
|
self.calls = []
|
|
159
167
|
|
|
160
|
-
def
|
|
168
|
+
def obtain(self, hosts, **kwargs):
|
|
161
169
|
self.calls.append({"hosts": hosts, "kwargs": kwargs})
|
|
162
170
|
return CertificateResponse()
|
|
163
171
|
|
|
@@ -171,7 +179,7 @@ def test_remote_obtain_self_verify_false_skips_server_prefilter():
|
|
|
171
179
|
assert manager.calls[0]["kwargs"]["self_verify"] is False
|
|
172
180
|
|
|
173
181
|
|
|
174
|
-
def
|
|
182
|
+
def test_remote_obtain_self_verify_true_delegates_strict_failure_to_manager():
|
|
175
183
|
app = Flask(__name__)
|
|
176
184
|
api = Api(app)
|
|
177
185
|
api_ns = Namespace("api")
|
|
@@ -184,8 +192,10 @@ def test_remote_obtain_self_verify_true_keeps_server_prefilter():
|
|
|
184
192
|
class Manager:
|
|
185
193
|
challenge_solvers = [Solver()]
|
|
186
194
|
|
|
187
|
-
def
|
|
188
|
-
|
|
195
|
+
def obtain(self, hosts, **kwargs):
|
|
196
|
+
assert hosts == ["force.example.com"]
|
|
197
|
+
assert kwargs["self_verify"] is True
|
|
198
|
+
raise ValueError("None of the domains are owned by this machine or could be verified")
|
|
189
199
|
|
|
190
200
|
create_api_resources(api_ns, Manager())
|
|
191
201
|
|
|
@@ -23,7 +23,7 @@ class DummyClient:
|
|
|
23
23
|
def set_handler(self, domain, handler):
|
|
24
24
|
self._handlers[domain] = handler
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def obtain(self, hosts, **kwargs):
|
|
27
27
|
host = hosts[0] if isinstance(hosts, list) else hosts
|
|
28
28
|
self.calls.append({"host": host, "kwargs": kwargs})
|
|
29
29
|
handler = self._handlers.get(host)
|
|
@@ -35,6 +35,9 @@ class DummyClient:
|
|
|
35
35
|
return handler(host, kwargs)
|
|
36
36
|
return handler
|
|
37
37
|
|
|
38
|
+
def issue_certificate(self, hosts, **kwargs):
|
|
39
|
+
return self.obtain(hosts, **kwargs)
|
|
40
|
+
|
|
38
41
|
|
|
39
42
|
class DummyClientWithObtain(DummyClient):
|
|
40
43
|
def __init__(self):
|
|
@@ -45,12 +48,12 @@ class DummyClientWithObtain(DummyClient):
|
|
|
45
48
|
def obtain(self, hosts, **kwargs):
|
|
46
49
|
host = hosts[0] if isinstance(hosts, list) else hosts
|
|
47
50
|
self.obtain_calls.append({"host": host, "kwargs": kwargs})
|
|
48
|
-
return super().
|
|
51
|
+
return super().obtain(hosts, **kwargs)
|
|
49
52
|
|
|
50
53
|
def issue_certificate(self, hosts, **kwargs):
|
|
51
54
|
host = hosts[0] if isinstance(hosts, list) else hosts
|
|
52
55
|
self.issue_calls.append({"host": host, "kwargs": kwargs})
|
|
53
|
-
return super().
|
|
56
|
+
return super().obtain(hosts, **kwargs)
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
class DummyRemoteClient(CertManagerClient):
|
|
@@ -408,6 +411,34 @@ def test_renewal_manager_prefers_obtain_when_available():
|
|
|
408
411
|
assert len(client.issue_calls) == 0
|
|
409
412
|
|
|
410
413
|
|
|
414
|
+
def test_renewal_manager_passes_typed_obtain_options_into_renewal_calls():
|
|
415
|
+
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
416
|
+
client = DummyClient()
|
|
417
|
+
cert = _make_cert_pem("batch.example.com", now, valid_for_days=60)
|
|
418
|
+
client.set_handler(
|
|
419
|
+
"batch.example.com",
|
|
420
|
+
CertificateResponse(issued=[IssuedCert(cert=cert, domains=["batch.example.com"])], existing=[]),
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
mgr = RenewalManager(
|
|
424
|
+
client,
|
|
425
|
+
renew_threshold_days=30,
|
|
426
|
+
clock_fn=lambda: now,
|
|
427
|
+
batch_domains=True,
|
|
428
|
+
self_verify=False,
|
|
429
|
+
organization="certapi-tests",
|
|
430
|
+
)
|
|
431
|
+
mgr.set_watch_domains(["batch.example.com"])
|
|
432
|
+
mgr.trigger_now()
|
|
433
|
+
|
|
434
|
+
assert len(client.calls) == 1
|
|
435
|
+
assert client.calls[0]["kwargs"]["batch_domains"] is True
|
|
436
|
+
assert client.calls[0]["kwargs"]["self_verify"] is False
|
|
437
|
+
assert client.calls[0]["kwargs"]["skip_failing"] is False
|
|
438
|
+
assert client.calls[0]["kwargs"]["renew_threshold_days"] == 30
|
|
439
|
+
assert client.calls[0]["kwargs"]["organization"] == "certapi-tests"
|
|
440
|
+
|
|
441
|
+
|
|
411
442
|
def test_local_keystore_seed_prevents_remote_call_when_fresh():
|
|
412
443
|
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
413
444
|
key = Key.generate("ecdsa")
|
|
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
|
{certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|