devnomads 0.2.4__tar.gz → 0.2.5__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.
- {devnomads-0.2.4 → devnomads-0.2.5}/PKG-INFO +36 -1
- {devnomads-0.2.4 → devnomads-0.2.5}/README.md +35 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/pyproject.toml +1 -1
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/acme/__init__.py +12 -1
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/acme/client.py +72 -2
- {devnomads-0.2.4 → devnomads-0.2.5}/tests/test_acme_client.py +104 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/.flake8 +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/.gitignore +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/.gitlab-ci.yml +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/Makefile +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/openapi.json +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/__init__.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/acme/challenge_server.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/acme/dns01.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/acme/errors.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/acme/http01.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/acme/keys.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/acme/verify.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/api/__init__.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/api/_services.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/api/client.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/api/credentials.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/api/errors.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/dns/__init__.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/dns/errors.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/dns/names.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/dns/zones.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/src/devnomads/py.typed +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tests/conftest.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tests/test_acme_challenges.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tests/test_acme_keys.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tests/test_client.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tests/test_credentials.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tests/test_dns.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tests/test_names.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tests/test_services.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tools/bump_version.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/tools/generate.py +0 -0
- {devnomads-0.2.4 → devnomads-0.2.5}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devnomads
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Python client library for the DevNomads API (transport, DNS, ACME)
|
|
5
5
|
Project-URL: Homepage, https://devnomads.nl
|
|
6
6
|
Author-email: DevNomads <support@devnomads.nl>
|
|
@@ -320,9 +320,16 @@ AcmeClient(
|
|
|
320
320
|
preferred_chain=None, # match alternate chain by CN
|
|
321
321
|
recursive_nameservers=None, # resolvers for propagation
|
|
322
322
|
user_agent="devnomads",
|
|
323
|
+
eab_kid=None, # external account binding key id
|
|
324
|
+
eab_hmac_key=None, # external account binding HMAC key
|
|
325
|
+
eab_hmac_alg="HS256", # HS256/HS384/HS512
|
|
323
326
|
)
|
|
324
327
|
```
|
|
325
328
|
|
|
329
|
+
Let's Encrypt remains the default CA. Any ACME v2 CA works by passing its
|
|
330
|
+
`directory_url`; CAs that mandate External Account Binding (such as ZeroSSL)
|
|
331
|
+
also need `eab_kid` and `eab_hmac_key`.
|
|
332
|
+
|
|
326
333
|
The account key at `account_key_path` is loaded if present and created
|
|
327
334
|
(mode 0600) otherwise.
|
|
328
335
|
|
|
@@ -419,6 +426,34 @@ acme = AcmeClient(
|
|
|
419
426
|
)
|
|
420
427
|
```
|
|
421
428
|
|
|
429
|
+
### Providers (Let's Encrypt, ZeroSSL)
|
|
430
|
+
|
|
431
|
+
`for_provider` resolves a named CA from `CA_DIRECTORIES`
|
|
432
|
+
(`letsencrypt`, `letsencrypt-staging`, `zerossl`) so you do not have to
|
|
433
|
+
remember directory URLs. Let's Encrypt stays the default everywhere else.
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from devnomads.acme import AcmeClient
|
|
437
|
+
|
|
438
|
+
# Let's Encrypt (default CA) - no EAB needed.
|
|
439
|
+
acme = AcmeClient.for_provider("letsencrypt", "/etc/devnomads/account.key")
|
|
440
|
+
|
|
441
|
+
# ZeroSSL - requires External Account Binding credentials from your
|
|
442
|
+
# ZeroSSL account (Developer -> EAB Credentials, or the REST API).
|
|
443
|
+
acme = AcmeClient.for_provider(
|
|
444
|
+
"zerossl",
|
|
445
|
+
"/etc/devnomads/zerossl-account.key",
|
|
446
|
+
contact_email="ops@example.com",
|
|
447
|
+
eab_kid="...", # EAB key id
|
|
448
|
+
eab_hmac_key="...", # base64url EAB HMAC key
|
|
449
|
+
)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Issuance is otherwise identical - the same `obtain_certificate` calls,
|
|
453
|
+
DNS-01/HTTP-01 solvers, and chain selection apply to every provider. The
|
|
454
|
+
directory URL and EAB fields can also be passed straight to the
|
|
455
|
+
`AcmeClient(...)` constructor if you prefer not to use the alias.
|
|
456
|
+
|
|
422
457
|
### Key helpers
|
|
423
458
|
|
|
424
459
|
```python
|
|
@@ -303,9 +303,16 @@ AcmeClient(
|
|
|
303
303
|
preferred_chain=None, # match alternate chain by CN
|
|
304
304
|
recursive_nameservers=None, # resolvers for propagation
|
|
305
305
|
user_agent="devnomads",
|
|
306
|
+
eab_kid=None, # external account binding key id
|
|
307
|
+
eab_hmac_key=None, # external account binding HMAC key
|
|
308
|
+
eab_hmac_alg="HS256", # HS256/HS384/HS512
|
|
306
309
|
)
|
|
307
310
|
```
|
|
308
311
|
|
|
312
|
+
Let's Encrypt remains the default CA. Any ACME v2 CA works by passing its
|
|
313
|
+
`directory_url`; CAs that mandate External Account Binding (such as ZeroSSL)
|
|
314
|
+
also need `eab_kid` and `eab_hmac_key`.
|
|
315
|
+
|
|
309
316
|
The account key at `account_key_path` is loaded if present and created
|
|
310
317
|
(mode 0600) otherwise.
|
|
311
318
|
|
|
@@ -402,6 +409,34 @@ acme = AcmeClient(
|
|
|
402
409
|
)
|
|
403
410
|
```
|
|
404
411
|
|
|
412
|
+
### Providers (Let's Encrypt, ZeroSSL)
|
|
413
|
+
|
|
414
|
+
`for_provider` resolves a named CA from `CA_DIRECTORIES`
|
|
415
|
+
(`letsencrypt`, `letsencrypt-staging`, `zerossl`) so you do not have to
|
|
416
|
+
remember directory URLs. Let's Encrypt stays the default everywhere else.
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
from devnomads.acme import AcmeClient
|
|
420
|
+
|
|
421
|
+
# Let's Encrypt (default CA) - no EAB needed.
|
|
422
|
+
acme = AcmeClient.for_provider("letsencrypt", "/etc/devnomads/account.key")
|
|
423
|
+
|
|
424
|
+
# ZeroSSL - requires External Account Binding credentials from your
|
|
425
|
+
# ZeroSSL account (Developer -> EAB Credentials, or the REST API).
|
|
426
|
+
acme = AcmeClient.for_provider(
|
|
427
|
+
"zerossl",
|
|
428
|
+
"/etc/devnomads/zerossl-account.key",
|
|
429
|
+
contact_email="ops@example.com",
|
|
430
|
+
eab_kid="...", # EAB key id
|
|
431
|
+
eab_hmac_key="...", # base64url EAB HMAC key
|
|
432
|
+
)
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Issuance is otherwise identical - the same `obtain_certificate` calls,
|
|
436
|
+
DNS-01/HTTP-01 solvers, and chain selection apply to every provider. The
|
|
437
|
+
directory URL and EAB fields can also be passed straight to the
|
|
438
|
+
`AcmeClient(...)` constructor if you prefer not to use the alias.
|
|
439
|
+
|
|
405
440
|
### Key helpers
|
|
406
441
|
|
|
407
442
|
```python
|
|
@@ -17,7 +17,14 @@ _EXTRA_MODULES = {"acme", "josepy", "cryptography", "dns"}
|
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
19
|
from .challenge_server import ChallengeServer
|
|
20
|
-
from .client import
|
|
20
|
+
from .client import (
|
|
21
|
+
CA_DIRECTORIES,
|
|
22
|
+
DEFAULT_DIRECTORY_URL,
|
|
23
|
+
LETSENCRYPT_DIRECTORY_URL,
|
|
24
|
+
LETSENCRYPT_STAGING_DIRECTORY_URL,
|
|
25
|
+
ZEROSSL_DIRECTORY_URL,
|
|
26
|
+
AcmeClient,
|
|
27
|
+
)
|
|
21
28
|
from .dns01 import DevNomadsDnsProvider, DnsProvider
|
|
22
29
|
from .errors import AcmeError
|
|
23
30
|
from .http01 import Http01Solver, StandaloneSolver, WebrootSolver
|
|
@@ -40,6 +47,10 @@ except ImportError as exc: # pragma: no cover - exercised via packaging
|
|
|
40
47
|
__all__ = [
|
|
41
48
|
"AcmeClient",
|
|
42
49
|
"DEFAULT_DIRECTORY_URL",
|
|
50
|
+
"LETSENCRYPT_DIRECTORY_URL",
|
|
51
|
+
"LETSENCRYPT_STAGING_DIRECTORY_URL",
|
|
52
|
+
"ZEROSSL_DIRECTORY_URL",
|
|
53
|
+
"CA_DIRECTORIES",
|
|
43
54
|
"AcmeError",
|
|
44
55
|
"DnsProvider",
|
|
45
56
|
"DevNomadsDnsProvider",
|
|
@@ -25,7 +25,25 @@ from .verify import verify_txt_record
|
|
|
25
25
|
|
|
26
26
|
log = logging.getLogger("devnomads.acme")
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
LETSENCRYPT_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory"
|
|
29
|
+
LETSENCRYPT_STAGING_DIRECTORY_URL = (
|
|
30
|
+
"https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
31
|
+
)
|
|
32
|
+
ZEROSSL_DIRECTORY_URL = "https://acme.zerossl.com/v2/DV90"
|
|
33
|
+
|
|
34
|
+
# Let's Encrypt remains the default CA everywhere it is not specified.
|
|
35
|
+
DEFAULT_DIRECTORY_URL = LETSENCRYPT_DIRECTORY_URL
|
|
36
|
+
|
|
37
|
+
# Named ACME providers, resolvable via :meth:`AcmeClient.for_provider`.
|
|
38
|
+
CA_DIRECTORIES = {
|
|
39
|
+
"letsencrypt": LETSENCRYPT_DIRECTORY_URL,
|
|
40
|
+
"letsencrypt-staging": LETSENCRYPT_STAGING_DIRECTORY_URL,
|
|
41
|
+
"zerossl": ZEROSSL_DIRECTORY_URL,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Providers that mandate External Account Binding (RFC 8555 §7.3.4) for new
|
|
45
|
+
# account registration.
|
|
46
|
+
_EAB_REQUIRED_PROVIDERS = frozenset({"zerossl"})
|
|
29
47
|
|
|
30
48
|
|
|
31
49
|
def _challenge_for_authz(challenge_types_map: dict[str, str], authz_body) -> str | None:
|
|
@@ -61,7 +79,14 @@ class AcmeClient:
|
|
|
61
79
|
preferred_chain: str | None = None,
|
|
62
80
|
recursive_nameservers: list[str] | None = None,
|
|
63
81
|
user_agent: str = "devnomads",
|
|
82
|
+
eab_kid: str | None = None,
|
|
83
|
+
eab_hmac_key: str | None = None,
|
|
84
|
+
eab_hmac_alg: str = "HS256",
|
|
64
85
|
) -> None:
|
|
86
|
+
if bool(eab_kid) != bool(eab_hmac_key):
|
|
87
|
+
raise AcmeError(
|
|
88
|
+
"external account binding requires both eab_kid and eab_hmac_key"
|
|
89
|
+
)
|
|
65
90
|
self.account_key_path = account_key_path
|
|
66
91
|
self.directory_url = directory_url
|
|
67
92
|
self.account_key_algorithm = account_key_algorithm
|
|
@@ -69,6 +94,39 @@ class AcmeClient:
|
|
|
69
94
|
self.preferred_chain = preferred_chain
|
|
70
95
|
self.recursive_nameservers = recursive_nameservers
|
|
71
96
|
self.user_agent = user_agent
|
|
97
|
+
self.eab_kid = eab_kid
|
|
98
|
+
self.eab_hmac_key = eab_hmac_key
|
|
99
|
+
self.eab_hmac_alg = eab_hmac_alg
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def for_provider(
|
|
103
|
+
cls, provider: str, account_key_path: str, **kwargs
|
|
104
|
+
) -> "AcmeClient":
|
|
105
|
+
"""Build a client for a named CA from :data:`CA_DIRECTORIES`.
|
|
106
|
+
|
|
107
|
+
``provider`` is one of ``"letsencrypt"`` (the default CA),
|
|
108
|
+
``"letsencrypt-staging"`` or ``"zerossl"``. Providers that mandate
|
|
109
|
+
External Account Binding (e.g. ZeroSSL) require ``eab_kid`` and
|
|
110
|
+
``eab_hmac_key`` keyword arguments. Any other keyword is forwarded to
|
|
111
|
+
:class:`AcmeClient`.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
directory_url = CA_DIRECTORIES[provider]
|
|
116
|
+
except KeyError:
|
|
117
|
+
raise AcmeError(
|
|
118
|
+
f"unknown ACME provider {provider!r}; "
|
|
119
|
+
f"known providers: {', '.join(sorted(CA_DIRECTORIES))}"
|
|
120
|
+
) from None
|
|
121
|
+
if provider in _EAB_REQUIRED_PROVIDERS and not (
|
|
122
|
+
kwargs.get("eab_kid") and kwargs.get("eab_hmac_key")
|
|
123
|
+
):
|
|
124
|
+
raise AcmeError(
|
|
125
|
+
f"provider {provider!r} requires external account binding; "
|
|
126
|
+
"pass eab_kid and eab_hmac_key"
|
|
127
|
+
)
|
|
128
|
+
kwargs.pop("directory_url", None)
|
|
129
|
+
return cls(account_key_path, directory_url=directory_url, **kwargs)
|
|
72
130
|
|
|
73
131
|
# -- account / client setup -------------------------------------------
|
|
74
132
|
|
|
@@ -84,11 +142,23 @@ class AcmeClient:
|
|
|
84
142
|
directory = messages.Directory.from_json(net.get(self.directory_url).json())
|
|
85
143
|
client = acme_client.ClientV2(directory, net)
|
|
86
144
|
|
|
87
|
-
|
|
145
|
+
registration_kwargs = dict(
|
|
88
146
|
contact=(f"mailto:{self.contact_email}",) if self.contact_email else (),
|
|
89
147
|
terms_of_service_agreed=True,
|
|
90
148
|
only_return_existing=False,
|
|
91
149
|
)
|
|
150
|
+
if self.eab_kid and self.eab_hmac_key:
|
|
151
|
+
log.debug("attaching external account binding (kid=%s)", self.eab_kid)
|
|
152
|
+
registration_kwargs["external_account_binding"] = (
|
|
153
|
+
messages.ExternalAccountBinding.from_data(
|
|
154
|
+
account_key.public_key(),
|
|
155
|
+
self.eab_kid,
|
|
156
|
+
self.eab_hmac_key,
|
|
157
|
+
directory,
|
|
158
|
+
hmac_alg=self.eab_hmac_alg,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
new_account = messages.NewRegistration(**registration_kwargs)
|
|
92
162
|
|
|
93
163
|
account = None
|
|
94
164
|
try:
|
|
@@ -122,4 +122,108 @@ def test_init_client_registers_with_new_registration(monkeypatch, tmp_path):
|
|
|
122
122
|
registration = captured["registration"]
|
|
123
123
|
assert isinstance(registration, messages.NewRegistration)
|
|
124
124
|
assert registration.contact == ("mailto:ops@example.com",)
|
|
125
|
+
# without EAB credentials, no external account binding is attached
|
|
126
|
+
assert not registration.external_account_binding
|
|
125
127
|
assert client.net.account is not None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _patch_fake_network(monkeypatch, client_mod, account_key, captured, directory):
|
|
131
|
+
from types import SimpleNamespace
|
|
132
|
+
|
|
133
|
+
class FakeResponse:
|
|
134
|
+
def json(self):
|
|
135
|
+
return {}
|
|
136
|
+
|
|
137
|
+
class FakeNetwork:
|
|
138
|
+
def __init__(self, *args, **kwargs):
|
|
139
|
+
self.account = None
|
|
140
|
+
|
|
141
|
+
def get(self, url):
|
|
142
|
+
return FakeResponse()
|
|
143
|
+
|
|
144
|
+
class FakeClientV2:
|
|
145
|
+
def __init__(self, directory, net):
|
|
146
|
+
self.net = net
|
|
147
|
+
|
|
148
|
+
def new_account(self, registration):
|
|
149
|
+
captured["registration"] = registration
|
|
150
|
+
return SimpleNamespace(uri="https://acme.example/acct/1")
|
|
151
|
+
|
|
152
|
+
monkeypatch.setattr(
|
|
153
|
+
client_mod, "load_or_create_account_key", lambda *a, **k: account_key
|
|
154
|
+
)
|
|
155
|
+
monkeypatch.setattr(client_mod.acme_client, "ClientNetwork", FakeNetwork)
|
|
156
|
+
monkeypatch.setattr(client_mod.acme_client, "ClientV2", FakeClientV2)
|
|
157
|
+
monkeypatch.setattr(
|
|
158
|
+
client_mod.messages.Directory, "from_json", lambda data: directory
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_init_client_attaches_external_account_binding(monkeypatch, tmp_path):
|
|
163
|
+
"""When EAB credentials are supplied, _init_client must attach an
|
|
164
|
+
externalAccountBinding to the NewRegistration (required by ZeroSSL)."""
|
|
165
|
+
|
|
166
|
+
from acme import messages
|
|
167
|
+
from cryptography.hazmat.backends import default_backend
|
|
168
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
169
|
+
from josepy import jwk
|
|
170
|
+
|
|
171
|
+
from devnomads.acme import client as client_mod
|
|
172
|
+
|
|
173
|
+
captured = {}
|
|
174
|
+
account_key = jwk.JWKEC(
|
|
175
|
+
key=ec.generate_private_key(ec.SECP256R1(), backend=default_backend())
|
|
176
|
+
)
|
|
177
|
+
directory = messages.Directory.from_json(
|
|
178
|
+
{"newAccount": "https://acme.example/new-acct"}
|
|
179
|
+
)
|
|
180
|
+
_patch_fake_network(monkeypatch, client_mod, account_key, captured, directory)
|
|
181
|
+
|
|
182
|
+
ac = client_mod.AcmeClient(
|
|
183
|
+
str(tmp_path / "account.pem"),
|
|
184
|
+
directory_url=client_mod.ZEROSSL_DIRECTORY_URL,
|
|
185
|
+
eab_kid="kid-123",
|
|
186
|
+
# HMAC key must be valid base64; this is "hmac-secret" encoded.
|
|
187
|
+
eab_hmac_key="aG1hYy1zZWNyZXQ",
|
|
188
|
+
)
|
|
189
|
+
ac._init_client()
|
|
190
|
+
|
|
191
|
+
registration = captured["registration"]
|
|
192
|
+
assert registration.external_account_binding # signed JWS dict
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_for_provider_resolves_directory_urls(tmp_path):
|
|
196
|
+
from devnomads.acme import client as client_mod
|
|
197
|
+
|
|
198
|
+
le = client_mod.AcmeClient.for_provider("letsencrypt", str(tmp_path / "a.pem"))
|
|
199
|
+
assert le.directory_url == client_mod.LETSENCRYPT_DIRECTORY_URL
|
|
200
|
+
|
|
201
|
+
zs = client_mod.AcmeClient.for_provider(
|
|
202
|
+
"zerossl",
|
|
203
|
+
str(tmp_path / "b.pem"),
|
|
204
|
+
eab_kid="kid-123",
|
|
205
|
+
eab_hmac_key="aG1hYy1zZWNyZXQ",
|
|
206
|
+
)
|
|
207
|
+
assert zs.directory_url == client_mod.ZEROSSL_DIRECTORY_URL
|
|
208
|
+
assert zs.eab_kid == "kid-123"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_for_provider_unknown_raises(tmp_path):
|
|
212
|
+
from devnomads.acme import client as client_mod
|
|
213
|
+
|
|
214
|
+
with pytest.raises(AcmeError):
|
|
215
|
+
client_mod.AcmeClient.for_provider("notaca", str(tmp_path / "a.pem"))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_for_provider_zerossl_requires_eab(tmp_path):
|
|
219
|
+
from devnomads.acme import client as client_mod
|
|
220
|
+
|
|
221
|
+
with pytest.raises(AcmeError):
|
|
222
|
+
client_mod.AcmeClient.for_provider("zerossl", str(tmp_path / "a.pem"))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_partial_eab_credentials_raise(tmp_path):
|
|
226
|
+
from devnomads.acme import client as client_mod
|
|
227
|
+
|
|
228
|
+
with pytest.raises(AcmeError):
|
|
229
|
+
client_mod.AcmeClient(str(tmp_path / "a.pem"), eab_kid="kid-only")
|
|
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
|