mirror of
https://github.com/certbot/certbot.git
synced 2026-08-01 16:19:13 +02:00
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:
@@ -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())
|
key = load_pem_private_key(data=privkey1, password=None, backend=default_backend())
|
||||||
|
|
||||||
assert isinstance(key, EllipticCurvePrivateKey)
|
assert isinstance(key, EllipticCurvePrivateKey), f"should be an EC key but was {type(key)}"
|
||||||
assert isinstance(key.curve, curve)
|
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:
|
def assert_rsa_key(key: str, key_size: Optional[int] = None) -> None:
|
||||||
|
|||||||
@@ -507,6 +507,19 @@ def test_new_key(context: IntegrationTestsContext) -> None:
|
|||||||
assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True')
|
assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True')
|
||||||
assert_elliptic_key(privkey4_path, SECP256R1)
|
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:
|
def test_incorrect_key_type(context: IntegrationTestsContext) -> None:
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
|
|||||||
@@ -324,12 +324,57 @@ def _avoid_invalidating_lineage(config: configuration.NamespaceConfig,
|
|||||||
"unless you use the --break-my-certs flag!")
|
"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]],
|
def renew_cert(config: configuration.NamespaceConfig, domains: Optional[List[str]],
|
||||||
le_client: client.Client, lineage: storage.RenewableCert) -> None:
|
le_client: client.Client, lineage: storage.RenewableCert) -> None:
|
||||||
"""Renew a certificate lineage."""
|
"""Renew a certificate lineage."""
|
||||||
renewal_params = lineage.configuration["renewalparams"]
|
renewal_params = lineage.configuration["renewalparams"]
|
||||||
original_server = renewal_params.get("server", cli.flag_default("server"))
|
original_server = renewal_params.get("server", cli.flag_default("server"))
|
||||||
_avoid_invalidating_lineage(config, lineage, original_server)
|
_avoid_invalidating_lineage(config, lineage, original_server)
|
||||||
|
_avoid_reuse_key_conflicts(config, lineage)
|
||||||
if not domains:
|
if not domains:
|
||||||
domains = lineage.names()
|
domains = lineage.names()
|
||||||
# The private key is the existing lineage private key if reuse_key is set.
|
# The private key is the existing lineage private key if reuse_key is set.
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ from typing import List
|
|||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import configobj
|
import configobj
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
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
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
import parsedatetime
|
import parsedatetime
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
@@ -569,6 +571,12 @@ class RenewableCert(interfaces.RenewableCert):
|
|||||||
return util.is_staging(self.server)
|
return util.is_staging(self.server)
|
||||||
return False
|
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:
|
def _check_symlinks(self) -> None:
|
||||||
"""Raises an exception if a symlink doesn't exist"""
|
"""Raises an exception if a symlink doesn't exist"""
|
||||||
for kind in ALL_FOUR:
|
for kind in ALL_FOUR:
|
||||||
@@ -1115,22 +1123,47 @@ class RenewableCert(interfaces.RenewableCert):
|
|||||||
target, values)
|
target, values)
|
||||||
return cls(new_config.filename, cli_config)
|
return cls(new_config.filename, cli_config)
|
||||||
|
|
||||||
@property
|
def _private_key(self) -> Union[RSAPrivateKey, EllipticCurvePrivateKey]:
|
||||||
def private_key_type(self) -> str:
|
|
||||||
"""
|
|
||||||
:returns: The type of algorithm for the private, RSA or ECDSA
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
with open(self.configuration["privkey"], "rb") as priv_key_file:
|
with open(self.configuration["privkey"], "rb") as priv_key_file:
|
||||||
key = load_pem_private_key(
|
key = load_pem_private_key(
|
||||||
data=priv_key_file.read(),
|
data=priv_key_file.read(),
|
||||||
password=None,
|
password=None,
|
||||||
backend=default_backend()
|
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):
|
if isinstance(key, RSAPrivateKey):
|
||||||
return "RSA"
|
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,
|
def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes,
|
||||||
new_chain: bytes, cli_config: configuration.NamespaceConfig) -> int:
|
new_chain: bytes, cli_config: configuration.NamespaceConfig) -> int:
|
||||||
|
|||||||
@@ -1209,6 +1209,7 @@ class MainTest(test_util.ConfigTestCase):
|
|||||||
mock_lineage.has_pending_deployment.return_value = False
|
mock_lineage.has_pending_deployment.return_value = False
|
||||||
mock_lineage.names.return_value = ['isnot.org']
|
mock_lineage.names.return_value = ['isnot.org']
|
||||||
mock_lineage.private_key_type = 'RSA'
|
mock_lineage.private_key_type = 'RSA'
|
||||||
|
mock_lineage.rsa_key_size = 2048
|
||||||
mock_certr = mock.MagicMock()
|
mock_certr = mock.MagicMock()
|
||||||
mock_key = mock.MagicMock(pem='pem_key')
|
mock_key = mock.MagicMock(pem='pem_key')
|
||||||
mock_client = mock.MagicMock()
|
mock_client = mock.MagicMock()
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ class RenewalTest(test_util.ConfigTestCase):
|
|||||||
self.assertEqual(self.config.webroot_map, {})
|
self.assertEqual(self.config.webroot_map, {})
|
||||||
self.assertEqual(self.config.webroot_path, ['/var/www/test'])
|
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.rsa_key_size = 'INVALID_VALUE'
|
||||||
self.config.reuse_key = True
|
self.config.reuse_key = True
|
||||||
self.config.dry_run = True
|
self.config.dry_run = True
|
||||||
@@ -75,7 +76,8 @@ class RenewalTest(test_util.ConfigTestCase):
|
|||||||
|
|
||||||
assert self.config.rsa_key_size == 2048
|
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.elliptic_curve = 'INVALID_CURVE'
|
||||||
self.config.reuse_key = True
|
self.config.reuse_key = True
|
||||||
self.config.dry_run = True
|
self.config.dry_run = True
|
||||||
@@ -99,7 +101,9 @@ class RenewalTest(test_util.ConfigTestCase):
|
|||||||
|
|
||||||
assert self.config.elliptic_curve == 'secp256r1'
|
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,
|
# 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.
|
# the key type, key parameters and reuse_key should be kept.
|
||||||
self.config.reuse_key = True
|
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.
|
# 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)
|
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()
|
@test_util.patch_display_util()
|
||||||
@mock.patch('certbot._internal.renewal.cli.set_by_cli')
|
@mock.patch('certbot._internal.renewal.cli.set_by_cli')
|
||||||
def test_remove_deprecated_config_elements(self, mock_set_by_cli, unused_mock_get_utility):
|
def test_remove_deprecated_config_elements(self, mock_set_by_cli, unused_mock_get_utility):
|
||||||
|
|||||||
Reference in New Issue
Block a user