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.
Files changed (39) hide show
  1. {devnomads-0.2.1 → devnomads-0.2.2}/PKG-INFO +1 -1
  2. {devnomads-0.2.1 → devnomads-0.2.2}/pyproject.toml +1 -1
  3. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/__init__.py +8 -1
  4. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/client.py +10 -2
  5. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/keys.py +21 -0
  6. {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_acme_client.py +12 -1
  7. {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_acme_keys.py +20 -0
  8. {devnomads-0.2.1 → devnomads-0.2.2}/uv.lock +1 -1
  9. {devnomads-0.2.1 → devnomads-0.2.2}/.flake8 +0 -0
  10. {devnomads-0.2.1 → devnomads-0.2.2}/.gitignore +0 -0
  11. {devnomads-0.2.1 → devnomads-0.2.2}/.gitlab-ci.yml +0 -0
  12. {devnomads-0.2.1 → devnomads-0.2.2}/Makefile +0 -0
  13. {devnomads-0.2.1 → devnomads-0.2.2}/README.md +0 -0
  14. {devnomads-0.2.1 → devnomads-0.2.2}/openapi.json +0 -0
  15. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/__init__.py +0 -0
  16. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/challenge_server.py +0 -0
  17. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/dns01.py +0 -0
  18. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/errors.py +0 -0
  19. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/http01.py +0 -0
  20. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/acme/verify.py +0 -0
  21. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/__init__.py +0 -0
  22. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/_services.py +0 -0
  23. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/client.py +0 -0
  24. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/credentials.py +0 -0
  25. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/api/errors.py +0 -0
  26. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/dns/__init__.py +0 -0
  27. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/dns/errors.py +0 -0
  28. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/dns/names.py +0 -0
  29. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/dns/zones.py +0 -0
  30. {devnomads-0.2.1 → devnomads-0.2.2}/src/devnomads/py.typed +0 -0
  31. {devnomads-0.2.1 → devnomads-0.2.2}/tests/conftest.py +0 -0
  32. {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_acme_challenges.py +0 -0
  33. {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_client.py +0 -0
  34. {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_credentials.py +0 -0
  35. {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_dns.py +0 -0
  36. {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_names.py +0 -0
  37. {devnomads-0.2.1 → devnomads-0.2.2}/tests/test_services.py +0 -0
  38. {devnomads-0.2.1 → devnomads-0.2.2}/tools/bump_version.py +0 -0
  39. {devnomads-0.2.1 → devnomads-0.2.2}/tools/generate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devnomads
3
- Version: 0.2.1
3
+ Version: 0.2.2
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: Loek Geleijn <support@devnomads.nl>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "devnomads"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Python client library for the DevNomads API (transport, DNS, ACME)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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 build_csr, generate_key, load_or_create_account_key, serialize_key
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 PrivateKey, build_csr, load_or_create_account_key, serialize_key
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(account_key, user_agent=self.user_agent)
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: object()
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())
@@ -468,7 +468,7 @@ wheels = [
468
468
 
469
469
  [[package]]
470
470
  name = "devnomads"
471
- version = "0.2.1"
471
+ version = "0.2.2"
472
472
  source = { editable = "." }
473
473
  dependencies = [
474
474
  { name = "httpx" },
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