error out when --reuse-key conflicts with other flags (#9262)

* error out when --reuse-key conflicts with other flags

* add unit test

* add integration tests

* lint
This commit is contained in:
alexzorin 2022-09-27 12:37:24 +10:00 committed by GitHub
parent c42dd567ca
commit 212c2ba990
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 141 additions and 13 deletions

View file

@ -33,8 +33,8 @@ def assert_elliptic_key(key: str, curve: Type[EllipticCurve]) -> None:
key = load_pem_private_key(data=privkey1, password=None, backend=default_backend())
assert isinstance(key, EllipticCurvePrivateKey)
assert isinstance(key.curve, curve)
assert isinstance(key, EllipticCurvePrivateKey), f"should be an EC key but was {type(key)}"
assert isinstance(key.curve, curve), f"should have curve {curve} but was {key.curve}"
def assert_rsa_key(key: str, key_size: Optional[int] = None) -> None:

View file

@ -507,6 +507,19 @@ def test_new_key(context: IntegrationTestsContext) -> None:
assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True')
assert_elliptic_key(privkey4_path, SECP256R1)
# certonly: it should not be possible to change a key parameter without --new-key
with pytest.raises(subprocess.CalledProcessError) as error:
context.certbot(['certonly', '-d', certname, '--reuse-key',
'--elliptic-curve', 'secp384r1'])
assert 'Unable to change the --elliptic-curve' in error.value.stderr
# certonly: not specifying --key-type should keep the existing key type (non-interactively).
# TODO: when ECDSA is made default key type, the key types must be inverted
context.certbot(['certonly', '-d', certname, '--no-reuse-key'])
privkey5, privkey5_path = private_key(5)
assert_elliptic_key(privkey5_path, SECP256R1)
assert privkey4 != privkey5
def test_incorrect_key_type(context: IntegrationTestsContext) -> None:
with pytest.raises(subprocess.CalledProcessError):

View file

@ -324,12 +324,57 @@ def _avoid_invalidating_lineage(config: configuration.NamespaceConfig,
"unless you use the --break-my-certs flag!")
def _avoid_reuse_key_conflicts(config: configuration.NamespaceConfig,
lineage: storage.RenewableCert) -> None:
"""Don't allow combining --reuse-key with any flags that would conflict
with key reuse (--key-type, --rsa-key-size, --elliptic-curve), unless
--new-key is also set.
"""
# If --no-reuse-key is set, no conflict
if cli.set_by_cli("reuse_key") and not config.reuse_key:
return
# If reuse_key is not set on the lineage and --reuse-key is not
# set on the CLI, no conflict.
if not lineage.reuse_key and not config.reuse_key:
return
# If --new-key is set, no conflict
if config.new_key:
return
kt = config.key_type.lower()
# The remaining cases where conflicts are present:
# - --key-type is set on the CLI and doesn't match the stored private key
# - It's an RSA key and --rsa-key-size is set and doesn't match
# - It's an ECDSA key and --eliptic-curve is set and doesn't match
potential_conflicts = [
("--key-type",
lambda: kt != lineage.private_key_type.lower()),
("--rsa-key-type",
lambda: kt == "rsa" and config.rsa_key_size != lineage.rsa_key_size),
("--elliptic-curve",
lambda: kt == "ecdsa" and lineage.elliptic_curve and \
config.elliptic_curve.lower() != lineage.elliptic_curve.lower())
]
for conflict in potential_conflicts:
if conflict[1]():
raise errors.Error(
f"Unable to change the {conflict[0]} of this certificate because --reuse-key "
"is set. To stop reusing the private key, specify --no-reuse-key. "
"To change the private key this one time and then reuse it in future, "
"add --new-key.")
def renew_cert(config: configuration.NamespaceConfig, domains: Optional[List[str]],
le_client: client.Client, lineage: storage.RenewableCert) -> None:
"""Renew a certificate lineage."""
renewal_params = lineage.configuration["renewalparams"]
original_server = renewal_params.get("server", cli.flag_default("server"))
_avoid_invalidating_lineage(config, lineage, original_server)
_avoid_reuse_key_conflicts(config, lineage)
if not domains:
domains = lineage.names()
# The private key is the existing lineage private key if reuse_key is set.

View file

@ -12,10 +12,12 @@ from typing import List
from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import Union
import configobj
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key
import parsedatetime
import pkg_resources
@ -569,6 +571,12 @@ class RenewableCert(interfaces.RenewableCert):
return util.is_staging(self.server)
return False
@property
def reuse_key(self) -> bool:
"""Returns whether this certificate is configured to reuse its private key"""
return "reuse_key" in self.configuration["renewalparams"] and \
self.configuration["renewalparams"].as_bool("reuse_key")
def _check_symlinks(self) -> None:
"""Raises an exception if a symlink doesn't exist"""
for kind in ALL_FOUR:
@ -1115,22 +1123,47 @@ class RenewableCert(interfaces.RenewableCert):
target, values)
return cls(new_config.filename, cli_config)
@property
def private_key_type(self) -> str:
"""
:returns: The type of algorithm for the private, RSA or ECDSA
:rtype: str
"""
def _private_key(self) -> Union[RSAPrivateKey, EllipticCurvePrivateKey]:
with open(self.configuration["privkey"], "rb") as priv_key_file:
key = load_pem_private_key(
data=priv_key_file.read(),
password=None,
backend=default_backend()
)
return key
@property
def private_key_type(self) -> str:
"""
:returns: The type of algorithm for the private, RSA or ECDSA
:rtype: str
"""
key = self._private_key()
if isinstance(key, RSAPrivateKey):
return "RSA"
else:
return "ECDSA"
return "ECDSA"
@property
def rsa_key_size(self) -> Optional[int]:
"""
:returns: If the private key is an RSA key, its size.
:rtype: int
"""
key = self._private_key()
if isinstance(key, RSAPrivateKey):
return key.key_size
return None
@property
def elliptic_curve(self) -> Optional[str]:
"""
:returns: If the private key is an elliptic key, the name of its curve.
:rtype: str
"""
key = self._private_key()
if isinstance(key, EllipticCurvePrivateKey):
return key.curve.name
return None
def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes,
new_chain: bytes, cli_config: configuration.NamespaceConfig) -> int:

View file

@ -1209,6 +1209,7 @@ class MainTest(test_util.ConfigTestCase):
mock_lineage.has_pending_deployment.return_value = False
mock_lineage.names.return_value = ['isnot.org']
mock_lineage.private_key_type = 'RSA'
mock_lineage.rsa_key_size = 2048
mock_certr = mock.MagicMock()
mock_key = mock.MagicMock(pem='pem_key')
mock_client = mock.MagicMock()

View file

@ -55,7 +55,8 @@ class RenewalTest(test_util.ConfigTestCase):
self.assertEqual(self.config.webroot_map, {})
self.assertEqual(self.config.webroot_path, ['/var/www/test'])
def test_reuse_key_renewal_params(self):
@mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts')
def test_reuse_key_renewal_params(self, unused_mock_avoid_reuse_conflicts):
self.config.rsa_key_size = 'INVALID_VALUE'
self.config.reuse_key = True
self.config.dry_run = True
@ -75,7 +76,8 @@ class RenewalTest(test_util.ConfigTestCase):
assert self.config.rsa_key_size == 2048
def test_reuse_ec_key_renewal_params(self):
@mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts')
def test_reuse_ec_key_renewal_params(self, unused_mock_avoid_reuse_conflicts):
self.config.elliptic_curve = 'INVALID_CURVE'
self.config.reuse_key = True
self.config.dry_run = True
@ -99,7 +101,9 @@ class RenewalTest(test_util.ConfigTestCase):
assert self.config.elliptic_curve == 'secp256r1'
def test_new_key(self):
@mock.patch('certbot._internal.renewal.cli.set_by_cli')
def test_new_key(self, mock_set_by_cli):
mock_set_by_cli.return_value = False
# When renewing with both reuse_key and new_key, the key should be regenerated,
# the key type, key parameters and reuse_key should be kept.
self.config.reuse_key = True
@ -125,6 +129,38 @@ class RenewalTest(test_util.ConfigTestCase):
# None is passed as the existing key, i.e. the key is not actually being reused.
le_client.obtain_certificate.assert_called_with(mock.ANY, None)
@mock.patch('certbot._internal.renewal.hooks.renew_hook')
@mock.patch('certbot._internal.renewal.cli.set_by_cli')
def test_reuse_key_conflicts(self, mock_set_by_cli, unused_mock_renew_hook):
mock_set_by_cli.return_value = False
# When renewing with reuse_key and a conflicting key parameter (size, curve)
# an error should be raised ...
self.config.reuse_key = True
self.config.key_type = "rsa"
self.config.rsa_key_size = 4096
self.config.dry_run = True
config = configuration.NamespaceConfig(self.config)
rc_path = test_util.make_lineage(
self.config.config_dir, 'sample-renewal.conf')
lineage = storage.RenewableCert(rc_path, config)
lineage.configuration["renewalparams"]["reuse_key"] = True
le_client = mock.MagicMock()
le_client.obtain_certificate.return_value = (None, None, None, None)
from certbot._internal import renewal
with self.assertRaisesRegex(errors.Error, "Unable to change the --rsa-key-type"):
renewal.renew_cert(self.config, None, le_client, lineage)
# ... unless --no-reuse-key is set
mock_set_by_cli.side_effect = lambda var: var == "reuse_key"
self.config.reuse_key = False
renewal.renew_cert(self.config, None, le_client, lineage)
@test_util.patch_display_util()
@mock.patch('certbot._internal.renewal.cli.set_by_cli')
def test_remove_deprecated_config_elements(self, mock_set_by_cli, unused_mock_get_utility):