mirror of
https://github.com/certbot/certbot.git
synced 2026-06-09 16:50:14 -04:00
parent
15024aabd3
commit
45626e88e2
6 changed files with 133 additions and 3 deletions
|
|
@ -6,7 +6,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
|
|||
|
||||
### Added
|
||||
|
||||
*
|
||||
* The --preferred-profile and --required-profile flags allow requesting a profile.
|
||||
https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -358,6 +358,16 @@ def prepare_and_parse_args(plugins: plugins_disco.PluginsRegistry, args: List[st
|
|||
default=flag_default("strict_permissions"),
|
||||
help="Require that all configuration files are owned by the current "
|
||||
"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(
|
||||
[None, "certonly", "renew", "run"],
|
||||
"--preferred-chain", dest="preferred_chain",
|
||||
|
|
|
|||
|
|
@ -471,8 +471,17 @@ class Client:
|
|||
"""
|
||||
if not self.acme:
|
||||
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:
|
||||
orderr = self.acme.new_order(csr_pem)
|
||||
orderr = self.acme.new_order(csr_pem, profile=profile)
|
||||
except acme_errors.WildcardUnsupportedError:
|
||||
raise errors.Error("The currently selected ACME CA endpoint does"
|
||||
" not support issuing wildcard certificates.")
|
||||
|
|
@ -485,7 +494,7 @@ class Client:
|
|||
deactivated, failed = self.auth_handler.deactivate_valid_authorizations(orderr)
|
||||
if deactivated:
|
||||
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:
|
||||
logger.warning("Certbot was unable to obtain fresh authorizations for every domain"
|
||||
". 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
|
||||
staple=None, # listed as False in help output
|
||||
strict_permissions=False,
|
||||
required_profile=None,
|
||||
preferred_profile=None,
|
||||
preferred_chain=None,
|
||||
pref_challs=[],
|
||||
validate_hooks=True,
|
||||
|
|
|
|||
|
|
@ -382,6 +382,92 @@ class ClientTest(ClientTestCommon):
|
|||
mock_crypto_util.cert_and_chain_from_fullchain.assert_called_once_with(
|
||||
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")
|
||||
def test_obtain_certificate_partial_success(self, mock_crypto_util):
|
||||
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
|
||||
|
||||
@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
|
||||
def preferred_chain(self) -> Optional[str]:
|
||||
"""Set the preferred certificate chain.
|
||||
|
|
|
|||
Loading…
Reference in a new issue