certapi 1.1.2__tar.gz → 1.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {certapi-1.1.2/src/certapi.egg-info → certapi-1.1.3}/PKG-INFO +1 -1
  2. {certapi-1.1.2 → certapi-1.1.3}/setup.py +1 -1
  3. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/client/cert_manager_client.py +4 -0
  4. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/manager/acme_cert_manager.py +13 -1
  5. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/server/api.py +16 -7
  6. {certapi-1.1.2 → certapi-1.1.3/src/certapi.egg-info}/PKG-INFO +1 -1
  7. {certapi-1.1.2 → certapi-1.1.3}/tests/test_acme_cert_manager_batching.py +34 -0
  8. {certapi-1.1.2 → certapi-1.1.3}/tests/test_obtain_interface.py +65 -0
  9. {certapi-1.1.2 → certapi-1.1.3}/MANIFEST.in +0 -0
  10. {certapi-1.1.2 → certapi-1.1.3}/README.md +0 -0
  11. {certapi-1.1.2 → certapi-1.1.3}/pyproject.toml +0 -0
  12. {certapi-1.1.2 → certapi-1.1.3}/setup.cfg +0 -0
  13. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/__init__.py +0 -0
  14. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/acme/Acme.py +0 -0
  15. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/acme/AcmeError.py +0 -0
  16. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/acme/Challenge.py +0 -0
  17. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/acme/Order.py +0 -0
  18. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/acme/__init__.py +0 -0
  19. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/acme/http.py +0 -0
  20. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
  21. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
  22. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +0 -0
  23. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/__init__.py +0 -0
  24. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/dns/__init__.py +0 -0
  25. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
  26. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
  27. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
  28. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
  29. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
  30. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
  31. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/cli.py +0 -0
  32. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/client/__init__.py +0 -0
  33. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/client/renewal_manager.py +0 -0
  34. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/crypto/__init__.py +0 -0
  35. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/crypto/crypto.py +0 -0
  36. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/crypto/crypto_classes.py +0 -0
  37. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/domain_batching.py +0 -0
  38. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/errors.py +0 -0
  39. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/http/HttpClientBase.py +0 -0
  40. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/http/__init__.py +0 -0
  41. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/http/types.py +0 -0
  42. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/issuers/AcmeCertIssuer.py +0 -0
  43. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/issuers/SelfCertIssuer.py +0 -0
  44. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/issuers/__init__.py +0 -0
  45. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/issuers/abstract_certissuer.py +0 -0
  46. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
  47. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/keystore/KeyStore.py +0 -0
  48. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
  49. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/keystore/RemoteKeyStore.py +0 -0
  50. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/keystore/SqliteKeyStore.py +0 -0
  51. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/keystore/__init__.py +0 -0
  52. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/manager/__init__.py +0 -0
  53. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/server/__init__.py +0 -0
  54. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/server/cert_api.py +0 -0
  55. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/server/key_api.py +0 -0
  56. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/util.py +0 -0
  57. {certapi-1.1.2 → certapi-1.1.3}/src/certapi/utils.py +0 -0
  58. {certapi-1.1.2 → certapi-1.1.3}/src/certapi.egg-info/SOURCES.txt +0 -0
  59. {certapi-1.1.2 → certapi-1.1.3}/src/certapi.egg-info/dependency_links.txt +0 -0
  60. {certapi-1.1.2 → certapi-1.1.3}/src/certapi.egg-info/entry_points.txt +0 -0
  61. {certapi-1.1.2 → certapi-1.1.3}/src/certapi.egg-info/requires.txt +0 -0
  62. {certapi-1.1.2 → certapi-1.1.3}/src/certapi.egg-info/top_level.txt +0 -0
  63. {certapi-1.1.2 → certapi-1.1.3}/tests/test_acme_error_handling.py +0 -0
  64. {certapi-1.1.2 → certapi-1.1.3}/tests/test_cert_issuer_generic.py +0 -0
  65. {certapi-1.1.2 → certapi-1.1.3}/tests/test_certs_with_key_types.py +0 -0
  66. {certapi-1.1.2 → certapi-1.1.3}/tests/test_cli.py +0 -0
  67. {certapi-1.1.2 → certapi-1.1.3}/tests/test_domain_batching_boulder_cases.py +0 -0
  68. {certapi-1.1.2 → certapi-1.1.3}/tests/test_http_error_handling.py +0 -0
  69. {certapi-1.1.2 → certapi-1.1.3}/tests/test_keystores.py +0 -0
  70. {certapi-1.1.2 → certapi-1.1.3}/tests/test_renewal_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: certapi
3
- Version: 1.1.2
3
+ Version: 1.1.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
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="certapi",
5
- version="1.1.2",
5
+ version="1.1.3",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -50,6 +50,7 @@ class CertManagerClient:
50
50
  renew_threshold_days: Optional[int] = None,
51
51
  skip_failing: bool = True,
52
52
  batch_domains: bool = False,
53
+ self_verify: bool = True,
53
54
  ) -> CertificateResponse:
54
55
  return self.obtain(
55
56
  hosts=hosts,
@@ -63,6 +64,7 @@ class CertManagerClient:
63
64
  renew_threshold_days=renew_threshold_days,
64
65
  skip_failing=skip_failing,
65
66
  batch_domains=batch_domains,
67
+ self_verify=self_verify,
66
68
  )
67
69
 
68
70
  def obtain(
@@ -78,6 +80,7 @@ class CertManagerClient:
78
80
  renew_threshold_days: Optional[int] = None,
79
81
  skip_failing: bool = True,
80
82
  batch_domains: bool = False,
83
+ self_verify: bool = True,
81
84
  ) -> CertificateResponse:
82
85
  params = {
83
86
  "hostname": hosts if isinstance(hosts, str) else hosts,
@@ -98,6 +101,7 @@ class CertManagerClient:
98
101
  params["renew_threshold_days"] = renew_threshold_days
99
102
  params["skip_failing"] = skip_failing
100
103
  params["batch_domains"] = batch_domains
104
+ params["self_verify"] = self_verify
101
105
 
102
106
  data = self._get("/api/obtain", params=params)
103
107
  res = CertificateResponse.from_json(data)
@@ -75,6 +75,7 @@ class AcmeCertManager:
75
75
  organization: Optional[str] = None,
76
76
  user_id: Optional[str] = None,
77
77
  renew_threshold_days: Optional[int] = None,
78
+ self_verify: bool = True,
78
79
  ) -> CertificateResponse:
79
80
  return self.obtain(
80
81
  hosts=hosts,
@@ -86,6 +87,7 @@ class AcmeCertManager:
86
87
  organization=organization,
87
88
  user_id=user_id,
88
89
  renew_threshold_days=renew_threshold_days,
90
+ self_verify=self_verify,
89
91
  )
90
92
 
91
93
  def obtain(
@@ -99,6 +101,7 @@ class AcmeCertManager:
99
101
  organization: Optional[str] = None,
100
102
  user_id: Optional[str] = None,
101
103
  renew_threshold_days: Optional[int] = None,
104
+ self_verify: bool = True,
102
105
  ) -> CertificateResponse:
103
106
  return self._issue_certificate_internal(
104
107
  hosts=hosts,
@@ -111,6 +114,7 @@ class AcmeCertManager:
111
114
  user_id=user_id,
112
115
  renew_threshold_days=renew_threshold_days,
113
116
  batch_generator=None,
117
+ self_verify=self_verify,
114
118
  )
115
119
 
116
120
  def issue_certificate_in_batches(
@@ -125,6 +129,7 @@ class AcmeCertManager:
125
129
  user_id: Optional[str] = None,
126
130
  renew_threshold_days: Optional[int] = None,
127
131
  batch_generator: Callable[[List[str]], List[List[str]]] = create_safe_domain_batches,
132
+ self_verify: bool = True,
128
133
  ) -> CertificateResponse:
129
134
  return self._issue_certificate_internal(
130
135
  hosts=hosts,
@@ -137,6 +142,7 @@ class AcmeCertManager:
137
142
  user_id=user_id,
138
143
  renew_threshold_days=renew_threshold_days,
139
144
  batch_generator=batch_generator,
145
+ self_verify=self_verify,
140
146
  )
141
147
 
142
148
  def _issue_certificate_internal(
@@ -151,6 +157,7 @@ class AcmeCertManager:
151
157
  user_id: Optional[str] = None,
152
158
  renew_threshold_days: Optional[int] = None,
153
159
  batch_generator: Optional[Callable[[List[str]], List[List[str]]]] = None,
160
+ self_verify: bool = True,
154
161
  ) -> CertificateResponse:
155
162
  if isinstance(hosts, str):
156
163
  hosts = [hosts]
@@ -174,7 +181,12 @@ class AcmeCertManager:
174
181
  for host in missing:
175
182
  found_store = None
176
183
  for store in self.challenge_solvers:
177
- if store.supports_domain(host):
184
+ supports_domain = (
185
+ store.supports_domain_strict(host)
186
+ if self_verify and hasattr(store, "supports_domain_strict")
187
+ else store.supports_domain(host)
188
+ )
189
+ if supports_domain:
178
190
  found_store = store
179
191
  break
180
192
  if found_store is not None:
@@ -103,6 +103,12 @@ def create_api_resources(api_ns, cert_manager: AcmeCertManager, renew_queue_size
103
103
  default=False,
104
104
  help="Issue certificates in separate safe batches instead of one combined order",
105
105
  )
106
+ obtain_parser.add_argument(
107
+ "self_verify",
108
+ type=inputs.boolean,
109
+ default=True,
110
+ help="Verify local challenge reachability/ownership before attempting issuance",
111
+ )
106
112
 
107
113
  @api_ns.route("/obtain")
108
114
  class ObtainCert(Resource):
@@ -117,16 +123,18 @@ def create_api_resources(api_ns, cert_manager: AcmeCertManager, renew_queue_size
117
123
  args = obtain_parser.parse_args()
118
124
  hostnames = args["hostname"]
119
125
  skip_failing = args.get("skip_failing", False)
126
+ self_verify = args.get("self_verify", True)
120
127
 
121
128
  with lock_manager:
122
- verified_hostnames = []
123
- for h in hostnames:
124
- # Find the first solver that supports this domain
125
- for solver in reversed(cert_manager.challenge_solvers):
126
- if solver.supports_domain_strict(h):
127
- verified_hostnames.append(h)
129
+ if self_verify:
130
+ verified_hostnames = []
131
+ for h in hostnames:
132
+ # Find the first solver that supports and can verify this domain.
133
+ for solver in reversed(cert_manager.challenge_solvers):
134
+ if solver.supports_domain_strict(h):
135
+ verified_hostnames.append(h)
128
136
 
129
- hostnames = verified_hostnames
137
+ hostnames = verified_hostnames
130
138
 
131
139
  if not hostnames and not skip_failing:
132
140
  api_ns.abort(400, message="None of the domains are owned by this machine or could be verified")
@@ -149,6 +157,7 @@ def create_api_resources(api_ns, cert_manager: AcmeCertManager, renew_queue_size
149
157
  organization=args["organization"],
150
158
  user_id=args["user_id"],
151
159
  renew_threshold_days=args.get("renew_threshold_days"),
160
+ self_verify=False,
152
161
  )
153
162
 
154
163
  print(data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: certapi
3
- Version: 1.1.2
3
+ Version: 1.1.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
@@ -22,6 +22,9 @@ class DummySolver:
22
22
  def supports_domain(self, _domain):
23
23
  return True
24
24
 
25
+ def supports_domain_strict(self, _domain):
26
+ return True
27
+
25
28
 
26
29
  class DummyIssuer:
27
30
  def __init__(self):
@@ -93,3 +96,34 @@ def test_issue_certificate_in_batches_uses_custom_batch_generator():
93
96
  ]
94
97
  assert len(response.issued) == 3
95
98
  assert [issued.domains for issued in response.issued] == issuer.calls
99
+
100
+
101
+ def test_issue_certificate_self_verify_false_skips_strict_solver_check():
102
+ class StrictFailingSolver(DummySolver):
103
+ def supports_domain_strict(self, _domain):
104
+ return False
105
+
106
+ key_store = DummyKeyStore()
107
+ issuer = DummyIssuer()
108
+ solver = StrictFailingSolver()
109
+ manager = AcmeCertManager(key_store=key_store, cert_issuer=issuer, challenge_solvers=[solver])
110
+
111
+ manager.issue_certificate(hosts=["force.example.com"], self_verify=False)
112
+
113
+ assert issuer.calls == [["force.example.com"]]
114
+
115
+
116
+ def test_issue_certificate_self_verify_true_uses_strict_solver_check():
117
+ class StrictFailingSolver(DummySolver):
118
+ def supports_domain_strict(self, _domain):
119
+ return False
120
+
121
+ key_store = DummyKeyStore()
122
+ issuer = DummyIssuer()
123
+ solver = StrictFailingSolver()
124
+ manager = AcmeCertManager(key_store=key_store, cert_issuer=issuer, challenge_solvers=[solver])
125
+
126
+ response = manager.issue_certificate(hosts=["force.example.com"])
127
+
128
+ assert issuer.calls == []
129
+ assert response.issued == []
@@ -1,8 +1,12 @@
1
1
  from unittest.mock import Mock
2
2
 
3
+ from flask import Flask
4
+ from flask_restx import Api, Namespace
5
+
3
6
  from certapi.client.cert_manager_client import CertManagerClient
4
7
  from certapi.http.types import CertificateResponse
5
8
  from certapi.manager.acme_cert_manager import AcmeCertManager
9
+ from certapi.server.api import create_api_resources
6
10
 
7
11
 
8
12
  def test_acme_cert_manager_issue_certificate_delegates_to_obtain(monkeypatch):
@@ -25,6 +29,7 @@ def test_acme_cert_manager_issue_certificate_delegates_to_obtain(monkeypatch):
25
29
  organization="Org",
26
30
  user_id="u1",
27
31
  renew_threshold_days=25,
32
+ self_verify=False,
28
33
  )
29
34
 
30
35
  assert res is expected
@@ -37,6 +42,7 @@ def test_acme_cert_manager_issue_certificate_delegates_to_obtain(monkeypatch):
37
42
  assert captured["organization"] == "Org"
38
43
  assert captured["user_id"] == "u1"
39
44
  assert captured["renew_threshold_days"] == 25
45
+ assert captured["self_verify"] is False
40
46
 
41
47
 
42
48
  def test_acme_cert_manager_obtain_uses_internal_issue_path(monkeypatch):
@@ -54,6 +60,7 @@ def test_acme_cert_manager_obtain_uses_internal_issue_path(monkeypatch):
54
60
  key_type="ecdsa",
55
61
  expiry_days=90,
56
62
  renew_threshold_days=30,
63
+ self_verify=False,
57
64
  )
58
65
 
59
66
  assert res is expected
@@ -62,6 +69,7 @@ def test_acme_cert_manager_obtain_uses_internal_issue_path(monkeypatch):
62
69
  assert captured["expiry_days"] == 90
63
70
  assert captured["renew_threshold_days"] == 30
64
71
  assert captured["batch_generator"] is None
72
+ assert captured["self_verify"] is False
65
73
 
66
74
 
67
75
  def test_cert_manager_client_issue_certificate_delegates_to_obtain(monkeypatch):
@@ -86,6 +94,7 @@ def test_cert_manager_client_issue_certificate_delegates_to_obtain(monkeypatch):
86
94
  renew_threshold_days=15,
87
95
  skip_failing=False,
88
96
  batch_domains=True,
97
+ self_verify=False,
89
98
  )
90
99
 
91
100
  assert res is expected
@@ -100,6 +109,7 @@ def test_cert_manager_client_issue_certificate_delegates_to_obtain(monkeypatch):
100
109
  assert captured["renew_threshold_days"] == 15
101
110
  assert captured["skip_failing"] is False
102
111
  assert captured["batch_domains"] is True
112
+ assert captured["self_verify"] is False
103
113
 
104
114
 
105
115
  def test_cert_manager_client_obtain_calls_api_obtain(monkeypatch):
@@ -118,6 +128,7 @@ def test_cert_manager_client_obtain_calls_api_obtain(monkeypatch):
118
128
  renew_threshold_days=12,
119
129
  skip_failing=False,
120
130
  batch_domains=True,
131
+ self_verify=False,
121
132
  )
122
133
 
123
134
  assert isinstance(res, CertificateResponse)
@@ -127,3 +138,57 @@ def test_cert_manager_client_obtain_calls_api_obtain(monkeypatch):
127
138
  assert captured["params"]["renew_threshold_days"] == 12
128
139
  assert captured["params"]["skip_failing"] is False
129
140
  assert captured["params"]["batch_domains"] is True
141
+ assert captured["params"]["self_verify"] is False
142
+
143
+
144
+ def test_remote_obtain_self_verify_false_skips_server_prefilter():
145
+ app = Flask(__name__)
146
+ api = Api(app)
147
+ api_ns = Namespace("api")
148
+ api.add_namespace(api_ns)
149
+
150
+ class Solver:
151
+ def supports_domain_strict(self, _domain):
152
+ return False
153
+
154
+ class Manager:
155
+ challenge_solvers = [Solver()]
156
+
157
+ def __init__(self):
158
+ self.calls = []
159
+
160
+ def issue_certificate(self, hosts, **kwargs):
161
+ self.calls.append({"hosts": hosts, "kwargs": kwargs})
162
+ return CertificateResponse()
163
+
164
+ manager = Manager()
165
+ create_api_resources(api_ns, manager)
166
+
167
+ response = app.test_client().get("/api/obtain?hostname=force.example.com&self_verify=false")
168
+
169
+ assert response.status_code == 200
170
+ assert manager.calls[0]["hosts"] == ["force.example.com"]
171
+ assert manager.calls[0]["kwargs"]["self_verify"] is False
172
+
173
+
174
+ def test_remote_obtain_self_verify_true_keeps_server_prefilter():
175
+ app = Flask(__name__)
176
+ api = Api(app)
177
+ api_ns = Namespace("api")
178
+ api.add_namespace(api_ns)
179
+
180
+ class Solver:
181
+ def supports_domain_strict(self, _domain):
182
+ return False
183
+
184
+ class Manager:
185
+ challenge_solvers = [Solver()]
186
+
187
+ def issue_certificate(self, hosts, **kwargs):
188
+ raise AssertionError("unverified host should not reach manager")
189
+
190
+ create_api_resources(api_ns, Manager())
191
+
192
+ response = app.test_client().get("/api/obtain?hostname=force.example.com")
193
+
194
+ assert response.status_code == 400
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