mirror of
https://github.com/certbot/certbot.git
synced 2026-08-01 02:44:21 +02:00
@@ -6,7 +6,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
*
|
* The --preferred-profile and --required-profile flags allow requesting a profile.
|
||||||
|
https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,16 @@ def prepare_and_parse_args(plugins: plugins_disco.PluginsRegistry, args: List[st
|
|||||||
default=flag_default("strict_permissions"),
|
default=flag_default("strict_permissions"),
|
||||||
help="Require that all configuration files are owned by the current "
|
help="Require that all configuration files are owned by the current "
|
||||||
"user; only needed if your config is somewhere unsafe like /tmp/")
|
"user; only needed if your config is somewhere unsafe like /tmp/")
|
||||||
|
helpful.add(
|
||||||
|
[None, "certonly", "renew", "run"],
|
||||||
|
"--required-profile", dest="required_profile",
|
||||||
|
default=flag_default("required_profile"), help=config_help("required_profile")
|
||||||
|
)
|
||||||
|
helpful.add(
|
||||||
|
[None, "certonly", "renew", "run"],
|
||||||
|
"--preferred-profile", dest="preferred_profile",
|
||||||
|
default=flag_default("preferred_profile"), help=config_help("preferred_profile")
|
||||||
|
)
|
||||||
helpful.add(
|
helpful.add(
|
||||||
[None, "certonly", "renew", "run"],
|
[None, "certonly", "renew", "run"],
|
||||||
"--preferred-chain", dest="preferred_chain",
|
"--preferred-chain", dest="preferred_chain",
|
||||||
|
|||||||
@@ -471,8 +471,17 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
if not self.acme:
|
if not self.acme:
|
||||||
raise errors.Error("ACME client is not set.")
|
raise errors.Error("ACME client is not set.")
|
||||||
|
|
||||||
|
profile = None
|
||||||
|
available_profiles = self.acme.directory.meta.profiles
|
||||||
|
preferred_profile = self.config.preferred_profile
|
||||||
|
if self.config.required_profile is not None:
|
||||||
|
profile = self.config.required_profile
|
||||||
|
elif (preferred_profile and available_profiles and
|
||||||
|
preferred_profile in available_profiles):
|
||||||
|
profile = preferred_profile
|
||||||
try:
|
try:
|
||||||
orderr = self.acme.new_order(csr_pem)
|
orderr = self.acme.new_order(csr_pem, profile=profile)
|
||||||
except acme_errors.WildcardUnsupportedError:
|
except acme_errors.WildcardUnsupportedError:
|
||||||
raise errors.Error("The currently selected ACME CA endpoint does"
|
raise errors.Error("The currently selected ACME CA endpoint does"
|
||||||
" not support issuing wildcard certificates.")
|
" not support issuing wildcard certificates.")
|
||||||
@@ -485,7 +494,7 @@ class Client:
|
|||||||
deactivated, failed = self.auth_handler.deactivate_valid_authorizations(orderr)
|
deactivated, failed = self.auth_handler.deactivate_valid_authorizations(orderr)
|
||||||
if deactivated:
|
if deactivated:
|
||||||
logger.debug("Recreating order after authz deactivations")
|
logger.debug("Recreating order after authz deactivations")
|
||||||
orderr = self.acme.new_order(csr_pem)
|
orderr = self.acme.new_order(csr_pem, profile=profile)
|
||||||
if failed:
|
if failed:
|
||||||
logger.warning("Certbot was unable to obtain fresh authorizations for every domain"
|
logger.warning("Certbot was unable to obtain fresh authorizations for every domain"
|
||||||
". The dry run will continue, but results may not be accurate.")
|
". The dry run will continue, but results may not be accurate.")
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ CLI_DEFAULTS: Dict[str, Any] = dict( # pylint: disable=use-dict-literal
|
|||||||
uir=None, # listed as False in help output
|
uir=None, # listed as False in help output
|
||||||
staple=None, # listed as False in help output
|
staple=None, # listed as False in help output
|
||||||
strict_permissions=False,
|
strict_permissions=False,
|
||||||
|
required_profile=None,
|
||||||
|
preferred_profile=None,
|
||||||
preferred_chain=None,
|
preferred_chain=None,
|
||||||
pref_challs=[],
|
pref_challs=[],
|
||||||
validate_hooks=True,
|
validate_hooks=True,
|
||||||
|
|||||||
@@ -382,6 +382,92 @@ class ClientTest(ClientTestCommon):
|
|||||||
mock_crypto_util.cert_and_chain_from_fullchain.assert_called_once_with(
|
mock_crypto_util.cert_and_chain_from_fullchain.assert_called_once_with(
|
||||||
self.eg_order.fullchain_pem)
|
self.eg_order.fullchain_pem)
|
||||||
|
|
||||||
|
@mock.patch("certbot._internal.client.crypto_util")
|
||||||
|
def test_obtain_certificate_no_profile_preference(self, mock_crypto_util):
|
||||||
|
csr = util.CSR(form="pem", file=None, data=CSR_SAN)
|
||||||
|
mock_crypto_util.generate_csr.return_value = csr
|
||||||
|
mock_crypto_util.generate_key.return_value = mock.sentinel.key
|
||||||
|
self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)
|
||||||
|
|
||||||
|
self.client.config.required_profile = None
|
||||||
|
self.client.config.preferred_profile = None
|
||||||
|
|
||||||
|
self._test_obtain_certificate_common(mock.sentinel.key, csr)
|
||||||
|
self.acme.new_order.assert_called_once_with(mock.ANY, profile=None)
|
||||||
|
|
||||||
|
@mock.patch("certbot._internal.client.crypto_util")
|
||||||
|
def test_obtain_certificate_required_profile(self, mock_crypto_util):
|
||||||
|
csr = util.CSR(form="pem", file=None, data=CSR_SAN)
|
||||||
|
mock_crypto_util.generate_csr.return_value = csr
|
||||||
|
mock_crypto_util.generate_key.return_value = mock.sentinel.key
|
||||||
|
self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)
|
||||||
|
|
||||||
|
self.client.config.required_profile = "exampleProfile"
|
||||||
|
self.client.config.preferred_profile = None
|
||||||
|
|
||||||
|
self._test_obtain_certificate_common(mock.sentinel.key, csr)
|
||||||
|
self.acme.new_order.assert_called_once_with(mock.ANY, profile="exampleProfile")
|
||||||
|
|
||||||
|
@mock.patch("certbot._internal.client.crypto_util")
|
||||||
|
def test_obtain_certificate_preferred_profile_exists(self, mock_crypto_util):
|
||||||
|
csr = util.CSR(form="pem", file=None, data=CSR_SAN)
|
||||||
|
mock_crypto_util.generate_csr.return_value = csr
|
||||||
|
mock_crypto_util.generate_key.return_value = mock.sentinel.key
|
||||||
|
self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)
|
||||||
|
|
||||||
|
self.client.config.required_profile = None
|
||||||
|
self.client.config.preferred_profile = "exampleProfile"
|
||||||
|
|
||||||
|
from acme.messages import Directory
|
||||||
|
self.acme.directory = Directory.from_json({
|
||||||
|
'meta': {
|
||||||
|
'profiles': {
|
||||||
|
'exampleProfile': 'here is some descriptive text, very informative',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self._test_obtain_certificate_common(mock.sentinel.key, csr)
|
||||||
|
self.acme.new_order.assert_called_once_with(mock.ANY, profile="exampleProfile")
|
||||||
|
|
||||||
|
@mock.patch("certbot._internal.client.crypto_util")
|
||||||
|
def test_obtain_certificate_preferred_profile_does_not_exist(self, mock_crypto_util):
|
||||||
|
csr = util.CSR(form="pem", file=None, data=CSR_SAN)
|
||||||
|
mock_crypto_util.generate_csr.return_value = csr
|
||||||
|
mock_crypto_util.generate_key.return_value = mock.sentinel.key
|
||||||
|
self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)
|
||||||
|
|
||||||
|
self.client.config.required_profile = None
|
||||||
|
self.client.config.preferred_profile = "thisProfileDoesNotExist"
|
||||||
|
|
||||||
|
from acme.messages import Directory
|
||||||
|
self.acme.directory = Directory.from_json({
|
||||||
|
'meta': {
|
||||||
|
'profiles': {
|
||||||
|
'example': 'profiles!',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self._test_obtain_certificate_common(mock.sentinel.key, csr)
|
||||||
|
self.acme.new_order.assert_called_once_with(mock.ANY, profile=None)
|
||||||
|
|
||||||
|
@mock.patch("certbot._internal.client.crypto_util")
|
||||||
|
def test_obtain_certificate_preferred_profile_no_profiles_exist(self, mock_crypto_util):
|
||||||
|
csr = util.CSR(form="pem", file=None, data=CSR_SAN)
|
||||||
|
mock_crypto_util.generate_csr.return_value = csr
|
||||||
|
mock_crypto_util.generate_key.return_value = mock.sentinel.key
|
||||||
|
self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)
|
||||||
|
|
||||||
|
self.client.config.required_profile = None
|
||||||
|
self.client.config.preferred_profile = "thisProfileDoesNotExist"
|
||||||
|
|
||||||
|
from acme.messages import Directory
|
||||||
|
self.acme.directory = Directory.from_json({})
|
||||||
|
|
||||||
|
self._test_obtain_certificate_common(mock.sentinel.key, csr)
|
||||||
|
self.acme.new_order.assert_called_once_with(mock.ANY, profile=None)
|
||||||
|
|
||||||
@mock.patch("certbot._internal.client.crypto_util")
|
@mock.patch("certbot._internal.client.crypto_util")
|
||||||
def test_obtain_certificate_partial_success(self, mock_crypto_util):
|
def test_obtain_certificate_partial_success(self, mock_crypto_util):
|
||||||
csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN)
|
csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN)
|
||||||
|
|||||||
@@ -363,6 +363,28 @@ class NamespaceConfig:
|
|||||||
"""
|
"""
|
||||||
return self.namespace.disable_renew_updates
|
return self.namespace.disable_renew_updates
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_profile(self) -> Optional[str]:
|
||||||
|
"""Request the given profile name from the ACME server.
|
||||||
|
|
||||||
|
If the ACME server returns an error, issuance (or renewal) will fail.
|
||||||
|
For long-term reliability, setting preferred_profile instead may be
|
||||||
|
preferable because it allows fallback to a default. Use this setting
|
||||||
|
when renewal failure is preferable to fallback.
|
||||||
|
"""
|
||||||
|
return self.namespace.required_profile
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preferred_profile(self) -> Optional[str]:
|
||||||
|
"""Request the given profile name from the ACME server, or fallback to default.
|
||||||
|
|
||||||
|
If the given profile name exists in the ACME directory, use it to request a
|
||||||
|
a certificate. Otherwise, fall back to requesting a certificate without a profile
|
||||||
|
(which means the CA will use its default profile). This allows renewals to
|
||||||
|
succeed even if the CA deprecates and removes a given profile.
|
||||||
|
"""
|
||||||
|
return self.namespace.preferred_profile
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def preferred_chain(self) -> Optional[str]:
|
def preferred_chain(self) -> Optional[str]:
|
||||||
"""Set the preferred certificate chain.
|
"""Set the preferred certificate chain.
|
||||||
|
|||||||
Reference in New Issue
Block a user