diff --git a/acme/src/acme/_internal/tests/client_test.py b/acme/src/acme/_internal/tests/client_test.py index d86bda7aa..fb900e38c 100644 --- a/acme/src/acme/_internal/tests/client_test.py +++ b/acme/src/acme/_internal/tests/client_test.py @@ -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 diff --git a/acme/src/acme/client.py b/acme/src/acme/client.py index 84ff64cbc..02ed8ad0b 100644 --- a/acme/src/acme/client.py +++ b/acme/src/acme/client.py @@ -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) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 144ba3b63..720c7bd64 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -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` diff --git a/certbot/src/certbot/_internal/client.py b/certbot/src/certbot/_internal/client.py index 67d464462..bcf3418b0 100644 --- a/certbot/src/certbot/_internal/client.py +++ b/certbot/src/certbot/_internal/client.py @@ -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]]