certbot: add --preferred-chain (#8080)

* acme: add support for alternative cert. chains

* certbot: add --preferred-chain

* remove support for issuer SKI matching

* show --preferred-chain in "run" help

* warn if no chain matched and it's not a dry-run

* fix existing failing tests

* add unit, integration tests

* bump acme dependency to dev version

* simplify test to avoid py2.7 recursion bug

* add preferred_chain to STR_CONFIG_ITEMS

* reduce preferred_chain warning to info level

* acme: fix some docstrings in .messages

* certbot: fix docstring in crypto_util

* try to fix certbot-nginx acme dep problem
This commit is contained in:
alexzorin
2020-06-30 17:45:39 -07:00
committed by GitHub
parent 2af297d72f
commit f743dbec3a
22 changed files with 311 additions and 25 deletions
+29 -5
View File
@@ -13,6 +13,7 @@ import josepy as jose
import OpenSSL import OpenSSL
import requests import requests
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from requests.utils import parse_header_links
from requests_toolbelt.adapters.source import SourceAddressAdapter from requests_toolbelt.adapters.source import SourceAddressAdapter
import six import six
from six.moves import http_client from six.moves import http_client
@@ -733,11 +734,13 @@ class ClientV2(ClientBase):
raise errors.ValidationError(failed) raise errors.ValidationError(failed)
return orderr.update(authorizations=responses) return orderr.update(authorizations=responses)
def finalize_order(self, orderr, deadline): def finalize_order(self, orderr, deadline, fetch_alternative_chains=False):
"""Finalize an order and obtain a certificate. """Finalize an order and obtain a certificate.
:param messages.OrderResource orderr: order to finalize :param messages.OrderResource orderr: order to finalize
:param datetime.datetime deadline: when to stop polling and timeout :param datetime.datetime deadline: when to stop polling and timeout
:param bool fetch_alternative_chains: whether to also fetch alternative
certificate chains
:returns: finalized order :returns: finalized order
:rtype: messages.OrderResource :rtype: messages.OrderResource
@@ -754,8 +757,13 @@ class ClientV2(ClientBase):
if body.error is not None: if body.error is not None:
raise errors.IssuanceError(body.error) raise errors.IssuanceError(body.error)
if body.certificate is not None: if body.certificate is not None:
certificate_response = self._post_as_get(body.certificate).text certificate_response = self._post_as_get(body.certificate)
return orderr.update(body=body, fullchain_pem=certificate_response) orderr = orderr.update(body=body, fullchain_pem=certificate_response.text)
if fetch_alternative_chains:
alt_chains_urls = self._get_links(certificate_response, 'alternate')
alt_chains = [self._post_as_get(url).text for url in alt_chains_urls]
orderr = orderr.update(alternative_fullchains_pem=alt_chains)
return orderr
raise errors.TimeoutError() raise errors.TimeoutError()
def revoke(self, cert, rsn): def revoke(self, cert, rsn):
@@ -785,6 +793,20 @@ class ClientV2(ClientBase):
new_args = args[:1] + (None,) + args[1:] new_args = args[:1] + (None,) + args[1:]
return self._post(*new_args, **kwargs) return self._post(*new_args, **kwargs)
def _get_links(self, response, relation_type):
"""
Retrieves all Link URIs of relation_type from the response.
:param requests.Response response: The requests HTTP response.
:param str relation_type: The relation type to filter by.
"""
# Can't use response.links directly because it drops multiple links
# of the same relation type, which is possible in RFC8555 responses.
if not 'Link' in response.headers:
return []
links = parse_header_links(response.headers['Link'])
return [l['url'] for l in links
if 'rel' in l and 'url' in l and l['rel'] == relation_type]
class BackwardsCompatibleClientV2(object): class BackwardsCompatibleClientV2(object):
"""ACME client wrapper that tends towards V2-style calls, but """ACME client wrapper that tends towards V2-style calls, but
@@ -863,11 +885,13 @@ class BackwardsCompatibleClientV2(object):
return messages.OrderResource(authorizations=authorizations, csr_pem=csr_pem) return messages.OrderResource(authorizations=authorizations, csr_pem=csr_pem)
return self.client.new_order(csr_pem) return self.client.new_order(csr_pem)
def finalize_order(self, orderr, deadline): def finalize_order(self, orderr, deadline, fetch_alternative_chains=False):
"""Finalize an order and obtain a certificate. """Finalize an order and obtain a certificate.
:param messages.OrderResource orderr: order to finalize :param messages.OrderResource orderr: order to finalize
:param datetime.datetime deadline: when to stop polling and timeout :param datetime.datetime deadline: when to stop polling and timeout
:param bool fetch_alternative_chains: whether to also fetch alternative
certificate chains
:returns: finalized order :returns: finalized order
:rtype: messages.OrderResource :rtype: messages.OrderResource
@@ -898,7 +922,7 @@ class BackwardsCompatibleClientV2(object):
chain = crypto_util.dump_pyopenssl_chain(chain).decode() chain = crypto_util.dump_pyopenssl_chain(chain).decode()
return orderr.update(fullchain_pem=(cert + chain)) return orderr.update(fullchain_pem=(cert + chain))
return self.client.finalize_order(orderr, deadline) return self.client.finalize_order(orderr, deadline, fetch_alternative_chains)
def revoke(self, cert, rsn): def revoke(self, cert, rsn):
"""Revoke certificate. """Revoke certificate.
+11 -4
View File
@@ -566,9 +566,11 @@ class Revocation(ResourceMixin, jose.JSONObjectWithFields):
class Order(ResourceBody): class Order(ResourceBody):
"""Order Resource Body. """Order Resource Body.
:ivar list of .Identifier: List of identifiers for the certificate. :ivar identifiers: List of identifiers for the certificate.
:vartype identifiers: `list` of `.Identifier`
:ivar acme.messages.Status status: :ivar acme.messages.Status status:
:ivar list of str authorizations: URLs of authorizations. :ivar authorizations: URLs of authorizations.
:vartype authorizations: `list` of `str`
:ivar str certificate: URL to download certificate as a fullchain PEM. :ivar str certificate: URL to download certificate as a fullchain PEM.
:ivar str finalize: URL to POST to to request issuance once all :ivar str finalize: URL to POST to to request issuance once all
authorizations have "valid" status. authorizations have "valid" status.
@@ -593,15 +595,20 @@ class OrderResource(ResourceWithURI):
:ivar acme.messages.Order body: :ivar acme.messages.Order body:
:ivar str csr_pem: The CSR this Order will be finalized with. :ivar str csr_pem: The CSR this Order will be finalized with.
:ivar list of acme.messages.AuthorizationResource authorizations: :ivar authorizations: Fully-fetched AuthorizationResource objects.
Fully-fetched AuthorizationResource objects. :vartype authorizations: `list` of `acme.messages.AuthorizationResource`
:ivar str fullchain_pem: The fetched contents of the certificate URL :ivar str fullchain_pem: The fetched contents of the certificate URL
produced once the order was finalized, if it's present. produced once the order was finalized, if it's present.
:ivar alternative_fullchains_pem: The fetched contents of alternative certificate
chain URLs produced once the order was finalized, if present and requested during
finalization.
:vartype alternative_fullchains_pem: `list` of `str`
""" """
body = jose.Field('body', decoder=Order.from_json) body = jose.Field('body', decoder=Order.from_json)
csr_pem = jose.Field('csr_pem', omitempty=True) csr_pem = jose.Field('csr_pem', omitempty=True)
authorizations = jose.Field('authorizations') authorizations = jose.Field('authorizations')
fullchain_pem = jose.Field('fullchain_pem', omitempty=True) fullchain_pem = jose.Field('fullchain_pem', omitempty=True)
alternative_fullchains_pem = jose.Field('alternative_fullchains_pem', omitempty=True)
@Directory.register @Directory.register
class NewOrder(Order): class NewOrder(Order):
+27 -1
View File
@@ -263,7 +263,7 @@ class BackwardsCompatibleClientV2Test(ClientTestBase):
with mock.patch('acme.client.ClientV2') as mock_client: with mock.patch('acme.client.ClientV2') as mock_client:
client = self._init() client = self._init()
client.finalize_order(mock_orderr, mock_deadline) client.finalize_order(mock_orderr, mock_deadline)
mock_client().finalize_order.assert_called_once_with(mock_orderr, mock_deadline) mock_client().finalize_order.assert_called_once_with(mock_orderr, mock_deadline, False)
def test_revoke(self): def test_revoke(self):
self.response.json.return_value = DIRECTORY_V1.to_json() self.response.json.return_value = DIRECTORY_V1.to_json()
@@ -842,6 +842,32 @@ class ClientV2Test(ClientTestBase):
deadline = datetime.datetime.now() - datetime.timedelta(seconds=60) deadline = datetime.datetime.now() - datetime.timedelta(seconds=60)
self.assertRaises(errors.TimeoutError, self.client.finalize_order, self.orderr, deadline) self.assertRaises(errors.TimeoutError, self.client.finalize_order, self.orderr, deadline)
def test_finalize_order_alt_chains(self):
updated_order = self.order.update(
certificate='https://www.letsencrypt-demo.org/acme/cert/',
)
updated_orderr = self.orderr.update(body=updated_order,
fullchain_pem=CERT_SAN_PEM,
alternative_fullchains_pem=[CERT_SAN_PEM,
CERT_SAN_PEM])
self.response.json.return_value = updated_order.to_json()
self.response.text = CERT_SAN_PEM
self.response.headers['Link'] ='<https://example.com/acme/cert/1>;rel="alternate", ' + \
'<https://exaple.com/dir>;rel="index", ' + \
'<https://example.com/acme/cert/2>;title="foo";rel="alternate"'
deadline = datetime.datetime(9999, 9, 9)
resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True)
self.net.post.assert_any_call('https://example.com/acme/cert/1',
mock.ANY, acme_version=2, new_nonce_url=mock.ANY)
self.net.post.assert_any_call('https://example.com/acme/cert/2',
mock.ANY, acme_version=2, new_nonce_url=mock.ANY)
self.assertEqual(resp, updated_orderr)
del self.response.headers['Link']
resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True)
self.assertEqual(resp, updated_orderr.update(alternative_fullchains_pem=[]))
def test_revoke(self): def test_revoke(self):
self.client.revoke(messages_test.CERT, self.rsn) self.client.revoke(messages_test.CERT, self.rsn)
self.net.post.assert_called_once_with( self.net.post.assert_called_once_with(
@@ -9,6 +9,8 @@ import shutil
import subprocess import subprocess
import time import time
from cryptography.x509 import NameOID
import pytest import pytest
from certbot_integration_tests.certbot_tests import context as certbot_context from certbot_integration_tests.certbot_tests import context as certbot_context
@@ -628,3 +630,31 @@ def test_dry_run_deactivate_authzs(context):
context.certbot(args) context.certbot(args)
with open(join(context.workspace, 'logs', 'letsencrypt.log'), 'r') as f: with open(join(context.workspace, 'logs', 'letsencrypt.log'), 'r') as f:
assert log_line in f.read(), 'Second order should have been recreated due to authz reuse' assert log_line in f.read(), 'Second order should have been recreated due to authz reuse'
def test_preferred_chain(context):
"""Test that --preferred-chain results in the correct chain.pem being produced"""
try:
issuers = misc.get_acme_issuers(context)
except NotImplementedError:
pytest.skip('This ACME server does not support alternative issuers.')
names = [i.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value \
for i in issuers]
domain = context.get_domain('preferred-chain')
cert_path = join(context.config_dir, 'live', domain, 'chain.pem')
conf_path = join(context.config_dir, 'renewal', '{}.conf'.format(domain))
for (requested, expected) in [(n, n) for n in names] + [('nonexistent', names[0])]:
args = ['certonly', '--cert-name', domain, '-d', domain,
'--preferred-chain', requested, '--force-renewal']
context.certbot(args)
dumped = misc.read_certificate(cert_path)
assert 'Issuer: CN={}'.format(expected) in dumped, \
'Expected chain issuer to be {} when preferring {}'.format(expected, requested)
with open(conf_path, 'r') as f:
assert 'preferred_chain = {}'.format(requested) in f.read(), \
'Expected preferred_chain to be set in renewal config'
@@ -130,6 +130,7 @@ class ACMEServer(object):
environ['PEBBLE_VA_NOSLEEP'] = '1' environ['PEBBLE_VA_NOSLEEP'] = '1'
environ['PEBBLE_WFE_NONCEREJECT'] = '0' environ['PEBBLE_WFE_NONCEREJECT'] = '0'
environ['PEBBLE_AUTHZREUSE'] = '100' environ['PEBBLE_AUTHZREUSE'] = '100'
environ['PEBBLE_ALTERNATE_ROOTS'] = str(PEBBLE_ALTERNATE_ROOTS)
self._launch_process( self._launch_process(
[pebble_path, '-config', pebble_config_path, '-dnsserver', '127.0.0.1:8053', '-strict'], [pebble_path, '-config', pebble_config_path, '-dnsserver', '127.0.0.1:8053', '-strict'],
@@ -7,3 +7,4 @@ BOULDER_V2_DIRECTORY_URL = 'http://localhost:4001/directory'
PEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir' PEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir'
PEBBLE_MANAGEMENT_URL = 'https://localhost:15000' PEBBLE_MANAGEMENT_URL = 'https://localhost:15000'
MOCK_OCSP_SERVER_PORT = 4002 MOCK_OCSP_SERVER_PORT = 4002
PEBBLE_ALTERNATE_ROOTS = 2
@@ -19,16 +19,30 @@ from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.serialization import NoEncryption from cryptography.hazmat.primitives.serialization import NoEncryption
from cryptography.hazmat.primitives.serialization import PrivateFormat from cryptography.hazmat.primitives.serialization import PrivateFormat
from cryptography.x509 import load_pem_x509_certificate
from OpenSSL import crypto from OpenSSL import crypto
import pkg_resources import pkg_resources
import requests import requests
from six.moves import SimpleHTTPServer from six.moves import SimpleHTTPServer
from six.moves import socketserver from six.moves import socketserver
from certbot_integration_tests.utils.constants import \
PEBBLE_ALTERNATE_ROOTS, PEBBLE_MANAGEMENT_URL
RSA_KEY_TYPE = 'rsa' RSA_KEY_TYPE = 'rsa'
ECDSA_KEY_TYPE = 'ecdsa' ECDSA_KEY_TYPE = 'ecdsa'
def _suppress_x509_verification_warnings():
try:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except ImportError:
# Handle old versions of request with vendorized urllib3
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
def check_until_timeout(url, attempts=30): def check_until_timeout(url, attempts=30):
""" """
Wait and block until given url responds with status 200, or raise an exception Wait and block until given url responds with status 200, or raise an exception
@@ -37,14 +51,7 @@ def check_until_timeout(url, attempts=30):
:param int attempts: the number of times to try to connect to the URL :param int attempts: the number of times to try to connect to the URL
:raise ValueError: exception raised if unable to reach the URL :raise ValueError: exception raised if unable to reach the URL
""" """
try: _suppress_x509_verification_warnings()
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except ImportError:
# Handle old versions of request with vendorized urllib3
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
for _ in range(attempts): for _ in range(attempts):
time.sleep(1) time.sleep(1)
try: try:
@@ -299,3 +306,23 @@ def echo(keyword, path=None):
.format(keyword)) .format(keyword))
return '{0} -c "from __future__ import print_function; print(\'{1}\')"{2}'.format( return '{0} -c "from __future__ import print_function; print(\'{1}\')"{2}'.format(
os.path.basename(sys.executable), keyword, ' >> "{0}"'.format(path) if path else '') os.path.basename(sys.executable), keyword, ' >> "{0}"'.format(path) if path else '')
def get_acme_issuers(context):
"""Gets the list of one or more issuer certificates from the ACME server used by the
context.
:param context: the testing context.
:return: the `list of x509.Certificate` representing the list of issuers.
"""
# TODO: in fact, Boulder has alternate chains in config-next/, just not yet in config/.
if context.acme_server != "pebble":
raise NotImplementedError()
_suppress_x509_verification_warnings()
issuers = []
for i in range(PEBBLE_ALTERNATE_ROOTS + 1):
request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediates/{}'.format(i), verify=False)
issuers.append(load_pem_x509_certificate(request.content, default_backend()))
return issuers
+1 -1
View File
@@ -1,3 +1,3 @@
# Remember to update setup.py to match the package versions below. # Remember to update setup.py to match the package versions below.
acme[dev]==1.4.0 -e acme[dev]
-e certbot[dev] -e certbot[dev]
+4
View File
@@ -11,6 +11,10 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
* Make Certbot snap find externally snapped plugins * Make Certbot snap find externally snapped plugins
* Function `certbot.compat.filesystem.umask` is a drop-in replacement for `os.umask` * Function `certbot.compat.filesystem.umask` is a drop-in replacement for `os.umask`
implementing umask for both UNIX and Windows systems. implementing umask for both UNIX and Windows systems.
* Support for alternative certificate chains in the `acme` module.
* Added `--preferred-chain <issuer CN>`. If a CA offers multiple certificate chains,
it may be used to indicate to Certbot which chain should be preferred.
* e.g. `--preferred-chain "DST Root CA X3"`
### Changed ### Changed
@@ -360,6 +360,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False):
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"],
"--preferred-chain", dest="preferred_chain",
default=flag_default("preferred_chain"), help=config_help("preferred_chain")
)
helpful.add( helpful.add(
["manual", "standalone", "certonly", "renew"], ["manual", "standalone", "certonly", "renew"],
"--preferred-challenges", dest="pref_challs", "--preferred-challenges", dest="pref_challs",
+10 -2
View File
@@ -288,8 +288,16 @@ class Client(object):
orderr = self._get_order_and_authorizations(csr.data, best_effort=False) orderr = self._get_order_and_authorizations(csr.data, best_effort=False)
deadline = datetime.datetime.now() + datetime.timedelta(seconds=90) deadline = datetime.datetime.now() + datetime.timedelta(seconds=90)
orderr = self.acme.finalize_order(orderr, deadline) get_alt_chains = self.config.preferred_chain is not None
cert, chain = crypto_util.cert_and_chain_from_fullchain(orderr.fullchain_pem) orderr = self.acme.finalize_order(orderr, deadline,
fetch_alternative_chains=get_alt_chains)
fullchain = orderr.fullchain_pem
if get_alt_chains and orderr.alternative_fullchains_pem:
fullchain = crypto_util.find_chain_with_issuer([fullchain] + \
orderr.alternative_fullchains_pem,
self.config.preferred_chain,
not self.config.dry_run)
cert, chain = crypto_util.cert_and_chain_from_fullchain(fullchain)
return cert.encode(), chain.encode() return cert.encode(), chain.encode()
def obtain_certificate(self, domains, old_keypath=None): def obtain_certificate(self, domains, old_keypath=None):
+1
View File
@@ -63,6 +63,7 @@ CLI_DEFAULTS = dict(
uir=None, uir=None,
staple=None, staple=None,
strict_permissions=False, strict_permissions=False,
preferred_chain=None,
pref_challs=[], pref_challs=[],
validate_hooks=True, validate_hooks=True,
directory_hooks=True, directory_hooks=True,
+2 -1
View File
@@ -34,7 +34,8 @@ logger = logging.getLogger(__name__)
# the renewal configuration process loses this information. # the renewal configuration process loses this information.
STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent", STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
"server", "account", "authenticator", "installer", "server", "account", "authenticator", "installer",
"renew_hook", "pre_hook", "post_hook", "http01_address"] "renew_hook", "pre_hook", "post_hook", "http01_address",
"preferred_chain"]
INT_CONFIG_ITEMS = ["rsa_key_size", "http01_port"] INT_CONFIG_ITEMS = ["rsa_key_size", "http01_port"]
BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names", "reuse_key", BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names", "reuse_key",
"autorenew"] "autorenew"]
+30
View File
@@ -530,3 +530,33 @@ def get_serial_from_cert(cert_path):
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, x509 = crypto.load_certificate(crypto.FILETYPE_PEM,
f.read()) f.read())
return x509.get_serial_number() return x509.get_serial_number()
def find_chain_with_issuer(fullchains, issuer_cn, warn_on_no_match=False):
"""Chooses the first certificate chain from fullchains which contains an
Issuer Subject Common Name matching issuer_cn.
:param fullchains: The list of fullchains in PEM chain format.
:type fullchains: `list` of `str`
:param `str` issuer_cn: The exact Subject Common Name to match against any
issuer in the certificate chain.
:returns: The best-matching fullchain, PEM-encoded, or the first if none match.
:rtype: `str`
"""
for chain in fullchains:
certs = [x509.load_pem_x509_certificate(cert, default_backend()) \
for cert in CERT_PEM_REGEX.findall(chain.encode())]
# Iterate the fullchain beginning from the leaf. For each certificate encountered,
# match against Issuer Subject CN.
for cert in certs:
cert_issuer_cn = cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if cert_issuer_cn and cert_issuer_cn[0].value == issuer_cn:
return chain
# Nothing matched, return whatever was first in the list.
if warn_on_no_match:
logger.info("Certbot has been configured to prefer certificate chains with "
"issuer '%s', but no chain from the CA matched this issuer. Using "
"the default certificate chain instead.", issuer_cn)
return fullchains[0]
+6
View File
@@ -254,6 +254,12 @@ class IConfig(zope.interface.Interface):
"If updates provided by installer enhancements when Certbot is being run" "If updates provided by installer enhancements when Certbot is being run"
" with \"renew\" verb should be disabled.") " with \"renew\" verb should be disabled.")
preferred_chain = zope.interface.Attribute(
"If the CA offers multiple certificate chains, prefer the chain with "
"an issuer matching this Subject Common Name. If no match, the default "
"offered chain will be used."
)
class IInstaller(IPlugin): class IInstaller(IPlugin):
"""Generic Certbot Installer Interface. """Generic Certbot Installer Interface.
+20
View File
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDUDCCAjigAwIBAgIIYbnTKswm95swDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgNzcwNjgzMCAXDTIwMDYxOTIxMDY1NVoYDzIwNTAw
NjE5MjAwNjU1WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDI1
OWJjMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALFeijP7HyxcUMrk
CuKgEqyaRmgjGj3JvPnm7RPJl0j6rgWe767eweP2dMJ8LnYHWZVDwmkysq1rb/kS
HuWoqUW1gpqwo4+ARMAb0fhx79Ze7eNp5M44PRN9SenK3TZQYrNFzC5t0NzNQaq8
ksv0R4kxi2VqfPuV6GCEYHI2t5z7249U+4PHEPSLEjq0TvFYp8aLVJ2cRLsspdXd
ANQwbKCWz00j03CcK79LXR6ncNw1WqEoYxjzmagiAKBf9kJVawxeJF91A+kPH052
hzGxhmMOU/VeWcl+zGeRR2cybyMF1qZ/tDX0n+J4q7FrcOjzJeZH8uxwOZ4N7Xn2
lqavdTkCAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMCE8MSR
KqrGnv0Vd4iv8FCVtVmhMB8GA1UdIwQYMBaAFBg/Nkzipd7/vMs/f9VV11uPo2bS
MA0GCSqGSIb3DQEBCwUAA4IBAQBISrMR9Fchj2u1FhxAr7eQsdM1Lus3B/eEJmcR
KDO/tNzns5zsrcJL42wzDVQA61X+aVzZBSfb1oMbwHCpWBvj88avL/mJ1OgrC+VZ
v7IDdKOwvXmFf0VVE5LLCPY+gfR65LVRkb0W9ZrZ9Oj0ke9A9ryPSNTYBvxE+Qar
V5Jqe0YdG7IQBd4xFFnpWZyEPnQBW2Sl5oudt6LgJaQosMJdRLp4lL805G/lKPEt
CKjWoaP6J4V1Ty+zwuWVdIpSUuIoWkZ7BeIIqVkTg7ieNj4eN2zddDxbTIquFD2K
7rZ1691krTWTe8bkLx6wGaAfvLOk97Ywmdsot3nTVRbaVTXq
-----END CERTIFICATE-----
+20
View File
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDUDCCAjigAwIBAgIILrJTDiWZDFkwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgMGNjNmYwMCAXDTIwMDYxOTIxMDY1NFoYDzIwNTAw
NjE5MjAwNjU0WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDI1
OWJjMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALFeijP7HyxcUMrk
CuKgEqyaRmgjGj3JvPnm7RPJl0j6rgWe767eweP2dMJ8LnYHWZVDwmkysq1rb/kS
HuWoqUW1gpqwo4+ARMAb0fhx79Ze7eNp5M44PRN9SenK3TZQYrNFzC5t0NzNQaq8
ksv0R4kxi2VqfPuV6GCEYHI2t5z7249U+4PHEPSLEjq0TvFYp8aLVJ2cRLsspdXd
ANQwbKCWz00j03CcK79LXR6ncNw1WqEoYxjzmagiAKBf9kJVawxeJF91A+kPH052
hzGxhmMOU/VeWcl+zGeRR2cybyMF1qZ/tDX0n+J4q7FrcOjzJeZH8uxwOZ4N7Xn2
lqavdTkCAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMCE8MSR
KqrGnv0Vd4iv8FCVtVmhMB8GA1UdIwQYMBaAFE4E9PDqHbaTzFi8XEeTc/s/xEal
MA0GCSqGSIb3DQEBCwUAA4IBAQCIEnLk9ZgIzwjew3ktKmZ/lDKl00EcIJ/or/GE
IM3exvEzUYJotoCKdw/6d1mjfsJ1AkJUrY5WASEhXgQ9wP5/z08xwmCSVowSnHhg
iKeQf6iNIqpxg7LGqOeSc32QXzeBMTsvQcqupZ7J6ptomiPopeAfYIMQWni9Ym/I
P1ZjydR2petIFioPuFtByu1UhvIcF4LCJ7UWwhVNcrivf5VGaRUVac4IxGZ4Urw+
W1xgT8pt5Rk1yCoqXsenllXRxdakbDDG9F0ZBWQNmvArYZJt1mdq1dBHT/JO/wKI
aGF5PXa6k3YwhyvK4zTDcwPKmOBikL2FgzlohLYfETpTznW0
-----END CERTIFICATE-----
+20
View File
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDWTCCAkGgAwIBAgIIYWk/LgSO+zowDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAyNTliYzEwHhcNMjAwNjIxMDEyMTM3
WhcNMjUwNjIxMDEyMTM3WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAOABmyVITyczj6/R/1EWH6FGEfzJgOFi
zUC1HweYouEnC3Pu4SV/qFyKC8FsVQAY5FYWV4rrexnMgAeCL7XBTFyvIYQ7FoCH
5SeyUr3T1j/BAaFZtdb419d+BXLBOYigIu36pyRrH/3z0oxvv0brG7zXty/AOHTK
/k5aE7e3iuUSPJqNoGIQi4NwtNw5Fwcpe4lOHRxNNp9kfjxKYzLgj9xaT6cw6r2T
9QJ7wnU9C0/cs9h98xz+IhApyoVWlF4SE87dpG99NQ8M92od5xv1jSIF34AMQzww
fAW3FIKr4onaMP6XHtiXnJFn4pSw+BryxPqsKz864Ritvokz/nJoylUCAwEAAaOB
mDCBlTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKG2yAaJtpPVStXakLLD5Jc7fcaN
MB8GA1UdIwQYMBaAFMCE8MSRKqrGnv0Vd4iv8FCVtVmhMBYGA1UdEQQPMA2CC2V4
YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBde/j8lgRqzksphFVwfx310TaD
4foS+15cq0OnTM/iVt1JtjyyGaRamgCcFXkm31IP8c9vnaReYGY/QawshK1NSLii
W6mN/RqguQNoovp0xw3SkDb+lyDo0VuIyMrbwlbhP4P5uue+Shb1ZTy7QmfUeYuK
OTjFI3j0oBiSKins3ryEFHIMvuXcaQ4F2uw6UOHBgwv5XbVV5B9l62jGbZnxrpc7
SB9mzU8I5CJoK1uetwuADwlhDX8ITQ778unR7yIZOojyIMKWd9z9KtKDVSPK9I+d
u0oYF4xG97nTks6Cqt2OUJizSY3ojgwK4U8w1795FCVUlq5Z58gpWf1BhA8a
-----END CERTIFICATE-----
+1 -1
View File
@@ -1,2 +1,2 @@
# Remember to update setup.py to match the package versions below. # Remember to update setup.py to match the package versions below.
acme[dev]==1.4.0 -e acme[dev]
+1 -1
View File
@@ -36,7 +36,7 @@ version = meta['version']
# specified here to avoid masking the more specific request requirements in # specified here to avoid masking the more specific request requirements in
# acme. See https://github.com/pypa/pip/issues/988 for more info. # acme. See https://github.com/pypa/pip/issues/988 for more info.
install_requires = [ install_requires = [
'acme>=1.4.0', 'acme>=1.6.0.dev0',
# We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but # We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but
# saying so here causes a runtime error against our temporary fork of 0.9.3 # saying so here causes a runtime error against our temporary fork of 0.9.3
# in which we added 2.6 support (see #2243), so we relax the requirement. # in which we added 2.6 support (see #2243), so we relax the requirement.
+15 -1
View File
@@ -274,7 +274,8 @@ class ClientTest(ClientTestCommon):
self.assertEqual(self.client.auth_handler.handle_authorizations.call_count, auth_count) self.assertEqual(self.client.auth_handler.handle_authorizations.call_count, auth_count)
self.acme.finalize_order.assert_called_once_with( self.acme.finalize_order.assert_called_once_with(
self.eg_order, mock.ANY) self.eg_order, mock.ANY,
fetch_alternative_chains=self.config.preferred_chain is not None)
@mock.patch("certbot._internal.client.crypto_util") @mock.patch("certbot._internal.client.crypto_util")
@mock.patch("certbot._internal.client.logger") @mock.patch("certbot._internal.client.logger")
@@ -293,9 +294,22 @@ class ClientTest(ClientTestCommon):
self.client.obtain_certificate_from_csr( self.client.obtain_certificate_from_csr(
test_csr, test_csr,
orderr=orderr)) orderr=orderr))
mock_crypto_util.find_chain_with_issuer.assert_not_called()
# and that the cert was obtained correctly # and that the cert was obtained correctly
self._check_obtain_certificate() self._check_obtain_certificate()
# Test that --preferred-chain results in chain selection
self.config.preferred_chain = "some issuer"
self.assertEqual(
(mock.sentinel.cert, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(
test_csr,
orderr=orderr))
mock_crypto_util.find_chain_with_issuer.assert_called_once_with(
[orderr.fullchain_pem] + orderr.alternative_fullchains_pem,
"some issuer", True)
self.config.preferred_chain = None
# Test for orderr=None # Test for orderr=None
self.assertEqual( self.assertEqual(
(mock.sentinel.cert, mock.sentinel.chain), (mock.sentinel.cert, mock.sentinel.chain),
+41
View File
@@ -27,6 +27,10 @@ SS_CERT = test_util.load_vector('cert_2048.pem')
P256_KEY = test_util.load_vector('nistp256_key.pem') P256_KEY = test_util.load_vector('nistp256_key.pem')
P256_CERT_PATH = test_util.vector_path('cert-nosans_nistp256.pem') P256_CERT_PATH = test_util.vector_path('cert-nosans_nistp256.pem')
P256_CERT = test_util.load_vector('cert-nosans_nistp256.pem') P256_CERT = test_util.load_vector('cert-nosans_nistp256.pem')
# CERT_LEAF is signed by CERT_ISSUER. CERT_ALT_ISSUER is a cross-sign of CERT_ISSUER.
CERT_LEAF = test_util.load_vector('cert_leaf.pem')
CERT_ISSUER = test_util.load_vector('cert_intermediate_1.pem')
CERT_ALT_ISSUER = test_util.load_vector('cert_intermediate_2.pem')
class InitSaveKeyTest(test_util.TempDirTestCase): class InitSaveKeyTest(test_util.TempDirTestCase):
"""Tests for certbot.crypto_util.init_save_key.""" """Tests for certbot.crypto_util.init_save_key."""
@@ -407,5 +411,42 @@ class CertAndChainFromFullchainTest(unittest.TestCase):
self.assertRaises(errors.Error, cert_and_chain_from_fullchain, cert_pem) self.assertRaises(errors.Error, cert_and_chain_from_fullchain, cert_pem)
class FindChainWithIssuerTest(unittest.TestCase):
"""Tests for certbot.crypto_util.find_chain_with_issuer"""
@classmethod
def _call(cls, fullchains, issuer_cn, **kwargs):
from certbot.crypto_util import find_chain_with_issuer
return find_chain_with_issuer(fullchains, issuer_cn, kwargs)
def _all_fullchains(self):
return [CERT_LEAF.decode() + CERT_ISSUER.decode(),
CERT_LEAF.decode() + CERT_ALT_ISSUER.decode()]
def test_positive_match(self):
"""Correctly pick the chain based on the root's CN"""
fullchains = self._all_fullchains()
matched = self._call(fullchains, "Pebble Root CA 0cc6f0")
self.assertEqual(matched, fullchains[1])
@mock.patch('certbot.crypto_util.logger.info')
def test_no_match(self, mock_info):
fullchains = self._all_fullchains()
matched = self._call(fullchains, "non-existent issuer")
self.assertEqual(matched, fullchains[0])
mock_info.assert_not_called()
@mock.patch('certbot.crypto_util.logger.info')
def test_warning_on_no_match(self, mock_info):
fullchains = self._all_fullchains()
matched = self._call(fullchains, "non-existent issuer",
warn_on_no_match=True)
self.assertEqual(matched, fullchains[0])
mock_info.assert_called_once_with("Certbot has been configured to prefer "
"certificate chains with issuer '%s', but no chain from the CA matched "
"this issuer. Using the default certificate chain instead.",
"non-existent issuer")
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() # pragma: no cover unittest.main() # pragma: no cover