devnomads 0.2.1__tar.gz → 0.2.2__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.1 → devnomads-0.2.2}/PKG-INFO +1 -1
- {devnomads-0.2.1 → devnomads-0.2.2}/pyproject.toml +1 -1
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/__init__.py +8 -1
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/client.py +10 -2
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/keys.py +21 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_acme_client.py +12 -1
- {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_acme_keys.py +20 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/uv.lock +1 -1
- {devnomads-0.2.1 → devnomads-0.2.2}/.flake8 +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/.gitignore +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/.gitlab-ci.yml +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/Makefile +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/README.md +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/openapi.json +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/__init__.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/challenge_server.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/dns01.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/errors.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/http01.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/verify.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/__init__.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/_services.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/client.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/credentials.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/errors.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/dns/__init__.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/dns/errors.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/dns/names.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/dns/zones.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/py.typed +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tests/conftest.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_acme_challenges.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_client.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_credentials.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_dns.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_names.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_services.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tools/bump_version.py +0 -0
- {devnomads-0.2.1 → devnomads-0.2.2}/tools/generate.py +0 -0
|
@@ -21,7 +21,13 @@ try:
|
|
|
21
21
|
from .dns01 import DevNomadsDnsProvider, DnsProvider
|
|
22
22
|
from .errors import AcmeError
|
|
23
23
|
from .http01 import Http01Solver, StandaloneSolver, WebrootSolver
|
|
24
|
-
from .keys import
|
|
24
|
+
from .keys import (
|
|
25
|
+
build_csr,
|
|
26
|
+
generate_key,
|
|
27
|
+
jws_alg,
|
|
28
|
+
load_or_create_account_key,
|
|
29
|
+
serialize_key,
|
|
30
|
+
)
|
|
25
31
|
from .verify import verify_txt_record
|
|
26
32
|
except ImportError as exc: # pragma: no cover - exercised via packaging
|
|
27
33
|
if (exc.name or "").split(".")[0] in _EXTRA_MODULES:
|
|
@@ -44,6 +50,7 @@ __all__ = [
|
|
|
44
50
|
"build_csr",
|
|
45
51
|
"generate_key",
|
|
46
52
|
"serialize_key",
|
|
53
|
+
"jws_alg",
|
|
47
54
|
"load_or_create_account_key",
|
|
48
55
|
"verify_txt_record",
|
|
49
56
|
]
|
|
@@ -14,7 +14,13 @@ from acme import messages
|
|
|
14
14
|
from .dns01 import DnsProvider
|
|
15
15
|
from .errors import AcmeError
|
|
16
16
|
from .http01 import Http01Solver
|
|
17
|
-
from .keys import
|
|
17
|
+
from .keys import (
|
|
18
|
+
PrivateKey,
|
|
19
|
+
build_csr,
|
|
20
|
+
jws_alg,
|
|
21
|
+
load_or_create_account_key,
|
|
22
|
+
serialize_key,
|
|
23
|
+
)
|
|
18
24
|
from .verify import verify_txt_record
|
|
19
25
|
|
|
20
26
|
log = logging.getLogger("devnomads.acme")
|
|
@@ -72,7 +78,9 @@ class AcmeClient:
|
|
|
72
78
|
account_key = load_or_create_account_key(
|
|
73
79
|
self.account_key_path, self.account_key_algorithm
|
|
74
80
|
)
|
|
75
|
-
net = acme_client.ClientNetwork(
|
|
81
|
+
net = acme_client.ClientNetwork(
|
|
82
|
+
account_key, alg=jws_alg(account_key), user_agent=self.user_agent
|
|
83
|
+
)
|
|
76
84
|
directory = messages.Directory.from_json(net.get(self.directory_url).json())
|
|
77
85
|
client = acme_client.ClientV2(directory, net)
|
|
78
86
|
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
|
|
11
|
+
import josepy
|
|
11
12
|
from cryptography import x509
|
|
12
13
|
from cryptography.hazmat.backends import default_backend
|
|
13
14
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
@@ -88,6 +89,26 @@ def load_or_create_account_key(path: str, algorithm: str):
|
|
|
88
89
|
raise AcmeError("unsupported account key type")
|
|
89
90
|
|
|
90
91
|
|
|
92
|
+
def jws_alg(account_key):
|
|
93
|
+
"""Return the JWS signing algorithm matching an account JWK.
|
|
94
|
+
|
|
95
|
+
``acme.client.ClientNetwork`` defaults to RS256, so pairing it with an
|
|
96
|
+
EC account key makes josepy reject the signature (the key is not an
|
|
97
|
+
instance of the algorithm's key type). The alg must follow the key.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
if isinstance(account_key, jwk.JWKRSA):
|
|
101
|
+
return josepy.RS256
|
|
102
|
+
if isinstance(account_key, jwk.JWKEC):
|
|
103
|
+
curve = account_key.key.curve.name
|
|
104
|
+
if curve == "secp256r1":
|
|
105
|
+
return josepy.ES256
|
|
106
|
+
if curve == "secp384r1":
|
|
107
|
+
return josepy.ES384
|
|
108
|
+
raise AcmeError(f"unsupported account key curve: {curve}")
|
|
109
|
+
raise AcmeError("unsupported account key type")
|
|
110
|
+
|
|
111
|
+
|
|
91
112
|
def build_csr(private_key: PrivateKey, identifiers: list[str]) -> bytes:
|
|
92
113
|
"""Build a PEM CSR: ``identifiers[0]`` is the CN, all are SANs."""
|
|
93
114
|
|
|
@@ -69,11 +69,18 @@ def test_init_client_registers_with_new_registration(monkeypatch, tmp_path):
|
|
|
69
69
|
it to new_account; this exercises the message construction that a wrong
|
|
70
70
|
class name (e.g. the non-existent NewAccount) would break."""
|
|
71
71
|
|
|
72
|
+
import josepy
|
|
72
73
|
from acme import messages
|
|
74
|
+
from cryptography.hazmat.backends import default_backend
|
|
75
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
76
|
+
from josepy import jwk
|
|
73
77
|
|
|
74
78
|
from devnomads.acme import client as client_mod
|
|
75
79
|
|
|
76
80
|
captured = {}
|
|
81
|
+
account_key = jwk.JWKEC(
|
|
82
|
+
key=ec.generate_private_key(ec.SECP256R1(), backend=default_backend())
|
|
83
|
+
)
|
|
77
84
|
|
|
78
85
|
class FakeResponse:
|
|
79
86
|
def json(self):
|
|
@@ -81,6 +88,7 @@ def test_init_client_registers_with_new_registration(monkeypatch, tmp_path):
|
|
|
81
88
|
|
|
82
89
|
class FakeNetwork:
|
|
83
90
|
def __init__(self, *args, **kwargs):
|
|
91
|
+
captured["alg"] = kwargs.get("alg")
|
|
84
92
|
self.account = None
|
|
85
93
|
|
|
86
94
|
def get(self, url):
|
|
@@ -95,7 +103,7 @@ def test_init_client_registers_with_new_registration(monkeypatch, tmp_path):
|
|
|
95
103
|
return SimpleNamespace(uri="https://acme.example/acct/1")
|
|
96
104
|
|
|
97
105
|
monkeypatch.setattr(
|
|
98
|
-
client_mod, "load_or_create_account_key", lambda *a, **k:
|
|
106
|
+
client_mod, "load_or_create_account_key", lambda *a, **k: account_key
|
|
99
107
|
)
|
|
100
108
|
monkeypatch.setattr(client_mod.acme_client, "ClientNetwork", FakeNetwork)
|
|
101
109
|
monkeypatch.setattr(client_mod.acme_client, "ClientV2", FakeClientV2)
|
|
@@ -108,6 +116,9 @@ def test_init_client_registers_with_new_registration(monkeypatch, tmp_path):
|
|
|
108
116
|
)
|
|
109
117
|
client, _account_key = ac._init_client()
|
|
110
118
|
|
|
119
|
+
# an EC account key must be signed with ES256, not the ClientNetwork
|
|
120
|
+
# default of RS256, or josepy rejects the JWS.
|
|
121
|
+
assert captured["alg"] == josepy.ES256
|
|
111
122
|
registration = captured["registration"]
|
|
112
123
|
assert isinstance(registration, messages.NewRegistration)
|
|
113
124
|
assert registration.contact == ("mailto:ops@example.com",)
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import stat
|
|
7
7
|
|
|
8
|
+
import josepy
|
|
8
9
|
import pytest
|
|
9
10
|
from cryptography import x509
|
|
10
11
|
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
|
@@ -14,9 +15,11 @@ from devnomads.acme import (
|
|
|
14
15
|
AcmeError,
|
|
15
16
|
build_csr,
|
|
16
17
|
generate_key,
|
|
18
|
+
jws_alg,
|
|
17
19
|
load_or_create_account_key,
|
|
18
20
|
serialize_key,
|
|
19
21
|
)
|
|
22
|
+
from devnomads.acme.keys import jwk
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def test_generate_rsa():
|
|
@@ -68,3 +71,20 @@ def test_account_key_reused(tmp_path):
|
|
|
68
71
|
content = open(path, "rb").read()
|
|
69
72
|
load_or_create_account_key(path, "ec256")
|
|
70
73
|
assert open(path, "rb").read() == content
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_jws_alg_rsa():
|
|
77
|
+
assert jws_alg(jwk.JWKRSA(key=generate_key("rsa2048"))) is josepy.RS256
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_jws_alg_ec256():
|
|
81
|
+
assert jws_alg(jwk.JWKEC(key=generate_key("ec256"))) is josepy.ES256
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_jws_alg_ec384():
|
|
85
|
+
assert jws_alg(jwk.JWKEC(key=generate_key("ec384"))) is josepy.ES384
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_jws_alg_unsupported_type():
|
|
89
|
+
with pytest.raises(AcmeError):
|
|
90
|
+
jws_alg(object())
|
|
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
|