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
@@ -9,6 +9,8 @@ import shutil
import subprocess
import time
from cryptography.x509 import NameOID
import pytest
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)
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'
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_WFE_NONCEREJECT'] = '0'
environ['PEBBLE_AUTHZREUSE'] = '100'
environ['PEBBLE_ALTERNATE_ROOTS'] = str(PEBBLE_ALTERNATE_ROOTS)
self._launch_process(
[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_MANAGEMENT_URL = 'https://localhost:15000'
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 NoEncryption
from cryptography.hazmat.primitives.serialization import PrivateFormat
from cryptography.x509 import load_pem_x509_certificate
from OpenSSL import crypto
import pkg_resources
import requests
from six.moves import SimpleHTTPServer
from six.moves import socketserver
from certbot_integration_tests.utils.constants import \
PEBBLE_ALTERNATE_ROOTS, PEBBLE_MANAGEMENT_URL
RSA_KEY_TYPE = 'rsa'
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):
"""
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
:raise ValueError: exception raised if unable to reach the URL
"""
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)
_suppress_x509_verification_warnings()
for _ in range(attempts):
time.sleep(1)
try:
@@ -299,3 +306,23 @@ def echo(keyword, path=None):
.format(keyword))
return '{0} -c "from __future__ import print_function; print(\'{1}\')"{2}'.format(
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