acme.ClientNetwork: JWK becomes optional (#10275)

This results in a ClientNetwork that can .get() but not .post(). Useful
for fetching ARI, which does not require authentication.
This commit is contained in:
Jacob Hoffman-Andrews 2025-05-06 12:34:50 -07:00 committed by GitHub
parent 2cf6cda1fa
commit 0075104805
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 25 additions and 10 deletions

View file

@ -693,12 +693,11 @@ class ClientNetworkTest(unittest.TestCase):
except requests.exceptions.ConnectionError as z: #pragma: no cover
assert "'Connection aborted.'" in str(z) or "[WinError 10061]" in str(z)
class ClientNetworkWithMockedResponseTest(unittest.TestCase):
"""Tests for acme.client.ClientNetwork which mock out response."""
def setUp(self):
self.net = ClientNetwork(key=None, alg=None)
self.net = ClientNetwork(key='fake', alg=None)
self.response = mock.MagicMock(ok=True, status_code=http_client.OK)
self.response.headers = {}
@ -851,6 +850,16 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
self.content_type = None
self.net.post('uri', self.obj, content_type=None, new_nonce_url='new_nonce_uri')
def test_no_key_error(self):
"A ClientNetwork with no key should error on POST but succeed on GET"
self.net = ClientNetwork()
self.net._send_request = mock.MagicMock()
self.net._send_request.return_value = self.response
with pytest.raises(errors.Error):
self.net.post('uri', "body")
assert self.response == self.net.get(
'uri', content_type=self.content_type, bar='baz')
if __name__ == '__main__':
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover

View file

@ -526,7 +526,7 @@ class ClientNetwork:
"""Initialize.
:param josepy.JWK key: Account private key
:param josepy.JWK key: Account private key. Required to use .post().
:param messages.RegistrationResource account: Account object. Required if you are
planning to use .post() for anything other than creating a new account;
may be set later after registering.
@ -535,7 +535,8 @@ class ClientNetwork:
:param str user_agent: String to send as User-Agent header.
:param int timeout: Timeout for requests.
"""
def __init__(self, key: jose.JWK, account: Optional[messages.RegistrationResource] = None,
def __init__(self, key: Optional[jose.JWK] = None,
account: Optional[messages.RegistrationResource] = None,
alg: jose.JWASignature = jose.RS256, verify_ssl: bool = True,
user_agent: str = 'acme-python', timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None:
self.key = key
@ -572,16 +573,17 @@ class ClientNetwork:
"""
jobj = obj.json_dumps(indent=2).encode() if obj else b''
logger.debug('JWS payload:\n%s', jobj)
assert self.key
kwargs = {
"alg": self.alg,
"nonce": nonce,
"url": url
"url": url,
"key": self.key
}
# newAccount and revokeCert work without the kid
# newAccount must not have kid
if self.account is not None:
kwargs["kid"] = self.account["uri"]
kwargs["key"] = self.key
return jws.JWS.sign(jobj, **cast(Mapping[str, Any], kwargs)).json_dumps(indent=2)
@classmethod
@ -771,6 +773,8 @@ class ClientNetwork:
def _post_once(self, url: str, obj: jose.JSONDeSerializable,
content_type: str = JOSE_CONTENT_TYPE, **kwargs: Any) -> requests.Response:
new_nonce_url = kwargs.pop('new_nonce_url', None)
if not self.key:
raise errors.Error("acme.ClientNetwork with no private key can't POST.")
data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url)
kwargs.setdefault('headers', {'Content-Type': content_type})
response = self._send_request('POST', url, data=data, **kwargs)

View file

@ -11,6 +11,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
### Changed
* Switched to src-layout from flat-layout to accommodate PEP 517 pip editable installs
* acme.client.ClientNetwork now makes the "key" parameter optional.
* Deprecated `acme.challenges.TLSALPN01Response`
* Deprecated `acme.challenges.TLSALPN01`
* Deprecated ivar `alpn_selection` from `acme.crypto_util.SSLSocket`

View file

@ -49,11 +49,12 @@ logger = logging.getLogger(__name__)
def acme_from_config_key(config: configuration.NamespaceConfig,
key: jose.JWK,
key: Optional[jose.JWK] = None,
regr: Optional[messages.RegistrationResource] = None,
) -> acme_client.ClientV2:
"""Wrangle ACME client construction"""
if key.typ == 'EC':
alg = RS256
if key and key.typ == 'EC':
public_key = key.key
if public_key.key_size == 256:
alg = ES256
@ -65,8 +66,6 @@ def acme_from_config_key(config: configuration.NamespaceConfig,
raise errors.NotSupportedError(
"No matching signing algorithm can be found for the key"
)
else:
alg = RS256
net = acme_client.ClientNetwork(key, alg=alg, account=regr,
verify_ssl=(not config.no_verify_ssl),
user_agent=determine_user_agent(config))
@ -225,6 +224,8 @@ def perform_registration(acme: acme_client.ClientV2, config: configuration.Names
:returns: Registration Resource.
:rtype: `acme.messages.RegistrationResource`
"""
if not acme.net.key:
raise errors.Error("acme client with no private key cannot register account.")
eab_credentials_supplied = config.eab_kid and config.eab_hmac_key
eab: Optional[Dict[str, Any]]