Second part: integration tests for certbot core

This commit is contained in:
Adrien Ferrand
2019-03-06 23:09:10 +01:00
parent 670e9d89b7
commit 1592d84291
8 changed files with 812 additions and 32 deletions
@@ -0,0 +1,45 @@
import os
import grp
__all__ = ['assert_hook_execution', 'assert_save_renew_hook', 'assert_certs_count_for_lineage',
'assert_equals_permissions', 'assert_equals_group_owner', 'assert_world_permissions']
def assert_hook_execution(probe_path, probe_content):
with open(probe_path, 'r') as file:
lines = file.readlines()
# Comparing pattern to each line avoids to match "pre" for a line with "pre-override"
assert [line for line in lines if line == '{0}{1}'.format(probe_content, os.linesep)]
def assert_save_renew_hook(config_dir, lineage):
assert os.path.isfile(os.path.join(config_dir, 'renewal/{0}.conf'.format(lineage)))
def assert_certs_count_for_lineage(config_dir, lineage, count):
archive_dir = os.path.join(config_dir, 'archive')
lineage_dir = os.path.join(archive_dir, lineage)
certs = [file for file in os.listdir(lineage_dir) if file.startswith('cert')]
assert len(certs) == count
def assert_equals_permissions(file1, file2, mask):
mode_file1 = os.stat(file1).st_mode & mask
mode_file2 = os.stat(file2).st_mode & mask
assert mode_file1 == mode_file2
def assert_equals_group_owner(file1, file2):
group_owner_file1 = grp.getgrgid(os.stat(file1).st_gid)[0]
group_owner_file2 = grp.getgrgid(os.stat(file2).st_gid)[0]
assert group_owner_file1 == group_owner_file2
def assert_world_permissions(file, mode):
mode_file_all = os.stat(file).st_mode & 0o007
assert mode_file_all == mode
@@ -0,0 +1,5 @@
import pytest
# Custom assertions defined in following package needs to be registered to be properly
# displayed in a pytest report when they are failing.
pytest.register_assert_rewrite('certbot_integration_tests.certbot_tests.assertions')
@@ -1,16 +1,105 @@
import os
import tempfile
import subprocess
import shutil
import sys
from distutils.version import LooseVersion
from certbot_integration_tests.utils import misc
class IntegrationTestsContext(object):
"""General fixture describing a certbot integration tests context"""
def __init__(self, request):
self.request = request
self.worker_id = request.config.slaveinput['slaveid'] if hasattr(request.config, 'slaveinput') else 'master'
if hasattr(request.config, 'slaveinput'): # Worker node
self.worker_id = request.config.slaveinput['slaveid']
self.acme_xdist = request.config.slaveinput['acme_xdist']
else: # Primary node
self.worker_id = 'primary'
self.acme_xdist = request.config.acme_xdist
self.acme_server =self.acme_xdist['acme_server']
self.directory_url = self.acme_xdist['directory_url']
self.tls_alpn_01_port = self.acme_xdist['https_port'][self.worker_id]
self.http_01_port = self.acme_xdist['http_port'][self.worker_id]
self.challtestsrv_mgt_port = self.acme_xdist['challtestsrv_port']
self.certbot_version = misc.get_certbot_version()
self.workspace = tempfile.mkdtemp()
self.config_dir = os.path.join(self.workspace, 'conf')
self.hook_probe = tempfile.mkstemp(dir=self.workspace)[1]
self.manual_dns_auth_hook = (
'{0} -c "import os; import requests; import json; '
"assert not os.environ.get('CERTBOT_DOMAIN').startswith('fail'); "
"data = {{'host':'_acme-challenge.{{0}}.'.format(os.environ.get('CERTBOT_DOMAIN')),"
"'value':os.environ.get('CERTBOT_VALIDATION')}}; "
"request = requests.post('http://localhost:{1}/set-txt', data=json.dumps(data)); "
"request.raise_for_status(); "
'"'
).format(sys.executable, self.challtestsrv_mgt_port)
self.manual_dns_cleanup_hook = (
'{0} -c "import os; import requests; import json; '
"data = {{'host':'_acme-challenge.{{0}}.'.format(os.environ.get('CERTBOT_DOMAIN'))}}; "
"request = requests.post('http://localhost:{1}/clear-txt', data=json.dumps(data)); "
"request.raise_for_status(); "
'"'
).format(sys.executable, self.challtestsrv_mgt_port)
def cleanup(self):
pass
shutil.rmtree(self.workspace)
def certbot_test_no_force_renew(self, args):
new_environ = os.environ.copy()
new_environ['TMPDIR'] = self.workspace
additional_args = []
if self.certbot_version >= LooseVersion('0.30.0'):
additional_args.append('--no-random-sleep-on-renew')
command = [
'certbot',
'--server', self.directory_url,
'--no-verify-ssl',
'--tls-sni-01-port', str(self.tls_alpn_01_port),
'--http-01-port', str(self.http_01_port),
'--manual-public-ip-logging-ok',
'--config-dir', self.config_dir,
'--work-dir', os.path.join(self.workspace, 'work'),
'--logs-dir', os.path.join(self.workspace, 'logs'),
'--non-interactive',
'--no-redirect',
'--agree-tos',
'--register-unsafely-without-email',
'--debug',
'-vv'
]
command.extend(args)
command.extend(additional_args)
print('Invoke command:\n{0}'.format(subprocess.list2cmdline(command)))
return subprocess.check_output(command, universal_newlines=True,
cwd=self.workspace, env=new_environ)
def certbot_test(self, args):
command = ['--renew-by-default']
command.extend(args)
return self.certbot_test_no_force_renew(command)
def common_no_force_renew(self, args):
command = ['--authenticator', 'standalone', '--installer', 'null']
command.extend(args)
return self.certbot_test_no_force_renew(command)
def common(self, args):
command = ['--renew-by-default']
command.extend(args)
return self.common_no_force_renew(command)
def wtf(self, prefix='le'):
return '{0}.{1}.wtf'.format(prefix, self.worker_id)
@@ -1,9 +1,15 @@
import requests
import urllib3
from __future__ import print_function
import subprocess
import shutil
import re
import os
from os.path import join, exists
import pytest
from certbot_integration_tests.certbot_tests.assertions import *
from certbot_integration_tests.certbot_tests import context as certbot_context
from certbot_integration_tests.utils import misc
@pytest.fixture()
@@ -16,25 +22,443 @@ def context(request):
integration_test_context.cleanup()
def test_hello_1(context):
assert context.http_01_port
assert context.tls_alpn_01_port
try:
response = requests.get(context.directory_url, verify=False)
response.raise_for_status()
assert response.json()
response.close()
except urllib3.exceptions.InsecureRequestWarning:
pass
def test_basic_commands(context):
# TMPDIR env variable is set to workspace for the certbot subprocess.
# So tempdir module will create any temporary files/dirs in workspace,
# and its content can be tested to check correct certbot cleanup.
initial_count_tmpfiles = len(os.listdir(context.workspace))
context.common(['--help'])
context.common(['--help', 'all'])
context.common(['--version'])
with pytest.raises(subprocess.CalledProcessError):
context.common(['--csr'])
new_count_tmpfiles = len(os.listdir(context.workspace))
assert initial_count_tmpfiles == new_count_tmpfiles
def test_hello_2(context):
assert context.http_01_port
assert context.tls_alpn_01_port
try:
response = requests.get(context.directory_url, verify=False)
response.raise_for_status()
assert response.json()
response.close()
except urllib3.exceptions.InsecureRequestWarning:
pass
def test_hook_dirs_creation(context):
context.common(['register'])
for hook_dir in misc.list_renewal_hooks_dirs(context.config_dir):
assert os.path.isdir(hook_dir)
def test_registration_override(context):
context.common(['register'])
context.common(['unregister'])
context.common(['register', '--email', 'ex1@domain.org,ex2@domain.org'])
# TODO: When `certbot register --update-registration` is fully deprecated,
# delete the two following deprecated uses
context.common(['register', '--update-registration', '--email', 'ex1@domain.org'])
context.common(['register', '--update-registration', '--email', 'ex1@domain.org,ex2@domain.org'])
context.common(['update_account', '--email', 'example@domain.org'])
context.common(['update_account', '--email', 'ex1@domain.org,ex2@domain.org'])
def test_prepare_plugins(context):
output = context.common(['plugins', '--init', '--prepare'])
assert 'webroot' in output
def test_http_01(context):
with misc.create_tcp_server(context.tls_alpn_01_port):
certname = context.wtf('le2')
context.common([
'--domains', certname, '--preferred-challenges', 'http-01', 'run',
'--cert-name', certname,
'--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe),
'--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe),
'--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe)
])
assert_hook_execution(context.hook_probe, 'deploy')
assert_save_renew_hook(context.config_dir, certname)
def test_manual_http_auth(context):
with misc.create_tcp_server(context.http_01_port) as webroot:
manual_http_hooks = misc.manual_http_hooks(webroot)
certname = context.wtf()
context.common([
'certonly', '-a', 'manual', '-d', certname,
'--cert-name', certname,
'--manual-auth-hook', manual_http_hooks[0],
'--manual-cleanup-hook', manual_http_hooks[1],
'--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe),
'--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe),
'--renew-hook', 'echo renew >> "{0}"'.format(context.hook_probe)
])
with pytest.raises(AssertionError):
assert_hook_execution(context.hook_probe, 'renew')
assert_save_renew_hook(context.config_dir, certname)
def test_manual_dns_auth(context):
certname = context.wtf('dns')
context.common([
'-a', 'manual', '-d', certname, '--preferred-challenges', 'dns',
'run', '--cert-name', certname,
'--manual-auth-hook', context.manual_dns_auth_hook,
'--manual-cleanup-hook', context.manual_dns_cleanup_hook,
'--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe),
'--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe),
'--renew-hook', 'echo renew >> "{0}"'.format(context.hook_probe)
])
with pytest.raises(AssertionError):
assert_hook_execution(context.hook_probe, 'renew')
assert_save_renew_hook(context.config_dir, certname)
def test_certonly(context):
context.common(['certonly', '--cert-name', 'newname', '-d', context.wtf('newname')])
def test_auth_and_install_with_csr(context):
certname = context.wtf('le3')
key_path = join(context.workspace, 'key.pem')
csr_path = join(context.workspace, 'csr.der')
misc.generate_csr([certname], key_path, csr_path)
cert_path = join(context.workspace, 'csr/cert.pem')
chain_path = join(context.workspace, 'csr/chain.pem')
context.common([
'auth', '--csr', csr_path,
'--cert-path', cert_path,
'--chain-path', chain_path
])
print(misc.read_certificate(cert_path))
print(misc.read_certificate(chain_path))
context.common([
'--domains', certname, 'install',
'--cert-path', cert_path,
'--key-path', key_path
])
def test_renew(context):
# First, we create a target certificate, with all hook dirs instantiated.
# We should have a new certificate, with hooks executed.
# Check also file permissions.
certname = context.wtf('renew')
context.common([
'certonly', '-d', certname, '--rsa-key-size', '4096',
'--preferred-challenges', 'http-01'
])
assert_certs_count_for_lineage(context.config_dir, certname, 1)
assert_world_permissions(
join(context.config_dir, 'archive/{0}/privkey1.pem'.format(certname)), 0)
# Second, we force renew, and ensure that renewal hooks files are executed.
# Also check that file permissions are correct.
misc.generate_test_file_hooks(context.config_dir, context.hook_probe)
context.common(['renew'])
assert_certs_count_for_lineage(context.config_dir, certname, 2)
assert_hook_execution(context.hook_probe, 'deploy')
assert_world_permissions(
join(context.config_dir, 'archive/{0}/privkey2.pem'.format(certname)), 0)
assert_equals_group_owner(
join(context.config_dir, 'archive/{0}/privkey1.pem'.format(certname)),
join(context.config_dir, 'archive/{0}/privkey2.pem'.format(certname)))
assert_equals_permissions(
join(context.config_dir, 'archive/{0}/privkey1.pem'.format(certname)),
join(context.config_dir, 'archive/{0}/privkey2.pem'.format(certname)), 0o074)
os.chmod(join(context.config_dir, 'archive/{0}/privkey2.pem'.format(certname)), 0o444)
# Third, we try to renew without force.
# It is not time, so no renew should occur, and no hooks should be executed.
open(context.hook_probe, 'w').close()
misc.generate_test_file_hooks(context.config_dir, context.hook_probe)
context.common_no_force_renew(['renew'])
assert_certs_count_for_lineage(context.config_dir, certname, 2)
with pytest.raises(AssertionError):
assert_hook_execution(context.hook_probe, 'deploy')
# Fourth, we modify the time when renew occur to 4 years before expiration.
# When trying renew without force, then renew should occur for this large time.
# Also we specify to not use hooks dir, so no hook should be run during this renew.
# Also this renew should use explicitly a 2048 key size.
# And finally we check the file permissions.
open(context.hook_probe, 'w').close()
with open(join(context.config_dir, 'renewal/{0}.conf'.format(certname)), 'r') as file:
lines = file.readlines()
lines.insert(4, 'renew_before_expiry = 100 years{0}'.format(os.linesep))
with open(join(context.config_dir, 'renewal/{0}.conf'.format(certname)), 'w') as file:
file.writelines(lines)
context.common_no_force_renew(['renew', '--no-directory-hooks',
'--rsa-key-size', '2048'])
assert_certs_count_for_lineage(context.config_dir, certname, 3)
with pytest.raises(AssertionError):
assert_hook_execution(context.hook_probe, 'deploy')
key2 = join(context.config_dir, 'archive/{0}/privkey2.pem'.format(certname))
key3 = join(context.config_dir, 'archive/{0}/privkey3.pem'.format(certname))
assert os.stat(key2).st_size > 3000 # 4096 bits keys takes more than 3000 bytes
assert os.stat(key3).st_size < 1800 # 2048 bits keys takes less than 1800 bytes
assert_world_permissions(
join(context.config_dir, 'archive/{0}/privkey3.pem'.format(certname)), 4)
assert_equals_group_owner(
join(context.config_dir, 'archive/{0}/privkey2.pem'.format(certname)),
join(context.config_dir, 'archive/{0}/privkey3.pem'.format(certname)))
assert_equals_permissions(
join(context.config_dir, 'archive/{0}/privkey2.pem'.format(certname)),
join(context.config_dir, 'archive/{0}/privkey3.pem'.format(certname)), 0o074)
# Fifth, we clean every dir hook, and replace their content by empty dir and empty files.
# Everything should renew correctly.
for hook_dir in misc.list_renewal_hooks_dirs(context.config_dir):
shutil.rmtree(hook_dir)
os.makedirs(join(hook_dir, 'dir'))
open(join(hook_dir, 'file'), 'w').close()
context.common(['renew'])
assert_certs_count_for_lineage(context.config_dir, certname, 4)
def test_hook_override(context):
certname = context.wtf('override')
context.common([
'certonly', '-d', certname,
'--preferred-challenges', 'http-01',
'--pre-hook', 'echo pre >> "{0}"'.format(context.hook_probe),
'--post-hook', 'echo post >> "{0}"'.format(context.hook_probe),
'--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe)
])
assert_hook_execution(context.hook_probe, 'pre')
assert_hook_execution(context.hook_probe, 'post')
assert_hook_execution(context.hook_probe, 'deploy')
open(context.hook_probe, 'w').close()
context.common([
'renew', '--cert-name', certname,
'--pre-hook', 'echo pre-override >> "{0}"'.format(context.hook_probe),
'--post-hook', 'echo post-override >> "{0}"'.format(context.hook_probe),
'--deploy-hook', 'echo deploy-override >> "{0}"'.format(context.hook_probe)
])
assert_hook_execution(context.hook_probe, 'pre-override')
assert_hook_execution(context.hook_probe, 'post-override')
assert_hook_execution(context.hook_probe, 'deploy-override')
with pytest.raises(AssertionError):
assert_hook_execution(context.hook_probe, 'pre')
with pytest.raises(AssertionError):
assert_hook_execution(context.hook_probe, 'post')
with pytest.raises(AssertionError):
assert_hook_execution(context.hook_probe, 'deploy')
open(context.hook_probe, 'w').close()
context.common(['renew', '--cert-name', certname])
assert_hook_execution(context.hook_probe, 'pre-override')
assert_hook_execution(context.hook_probe, 'post-override')
assert_hook_execution(context.hook_probe, 'deploy-override')
def test_invalid_domain_with_dns_challenge(context):
certs = ','.join([context.wtf('dns1'), context.wtf('fail-dns1')])
context.common([
'-a', 'manual', '-d', certs,
'--allow-subset-of-names',
'--preferred-challenges', 'dns',
'--manual-auth-hook', context.manual_dns_auth_hook,
'--manual-cleanup-hook', context.manual_dns_cleanup_hook
])
output = context.common(['certificates'])
assert context.wtf('fail-dns1') not in output
def test_reuse_key(context):
certname = context.wtf('reusekey')
context.common(['--domains', certname, '--reuse-key'])
context.common(['renew', '--cert-name', certname])
with open(join(context.config_dir, 'archive/{0}/privkey1.pem').format(certname), 'r') as file:
privkey1 = file.read()
with open(join(context.config_dir, 'archive/{0}/privkey2.pem').format(certname), 'r') as file:
privkey2 = file.read()
assert privkey1 == privkey2
context.common(['--cert-name', certname, '--domains', certname, '--force-renewal'])
with open(join(context.config_dir, 'archive/{0}/privkey3.pem').format(certname), 'r') as file:
privkey3 = file.read()
assert privkey2 != privkey3
with open(join(context.config_dir, 'archive/{0}/cert1.pem').format(certname), 'r') as file:
cert1 = file.read()
with open(join(context.config_dir, 'archive/{0}/cert2.pem').format(certname), 'r') as file:
cert2 = file.read()
with open(join(context.config_dir, 'archive/{0}/cert3.pem').format(certname), 'r') as file:
cert3 = file.read()
assert len({cert1, cert2, cert3}) == 3
def test_ecdsa(context):
key_path = join(context.workspace, 'privkey-p384.pem')
csr_path = join(context.workspace, 'csr-p384.der')
cert_path = join(context.workspace, 'cert-p384.pem')
chain_path = join(context.workspace, 'chain-p384.pem')
misc.generate_csr([context.wtf('ecdsa')], key_path, csr_path, key_type='ECDSA')
context.common(['auth', '--csr', csr_path, '--cert-path', cert_path, '--chain-path', chain_path])
certificate = misc.read_certificate(cert_path)
assert 'ASN1 OID: secp384r1' in certificate
def test_ocsp_must_staple(context):
certname = context.wtf('must-staple')
context.common(['auth', '--must-staple', '--domains', certname])
certificate = misc.read_certificate(join(context.config_dir,
'live/{0}/cert.pem').format(certname))
assert 'status_request' in certificate or '1.3.6.1.5.5.7.1.24'
def test_revoke_simple(context):
certname = context.wtf()
cert_path = join(context.config_dir, 'live/{0}/cert.pem'.format(certname))
context.common(['-d', certname])
context.common(['revoke', '--cert-path', cert_path, '--delete-after-revoke'])
assert not exists(cert_path)
certname = context.wtf('le1')
cert_path = join(context.config_dir, 'live/{0}/cert.pem'.format(certname))
context.common(['-d', certname])
context.common(['revoke', '--cert-path', cert_path, '--no-delete-after-revoke'])
assert exists(cert_path)
context.common(['delete', '--cert-name', certname])
assert not exists(join(context.config_dir, 'archive/{0}'.format(certname)))
assert not exists(join(context.config_dir, 'live/{0}'.format(certname)))
assert not exists(join(context.config_dir, 'renewal/{0}.conf').format(certname))
certname = context.wtf('le2')
key_path = join(context.config_dir, 'live/{0}/privkey.pem'.format(certname))
cert_path = join(context.config_dir, 'live/{0}/cert.pem'.format(certname))
context.common(['-d', certname])
context.common(['revoke', '--cert-path', cert_path, '--key-path', key_path])
def test_revoke_and_unregister(context):
cert1 = context.wtf('le1')
cert2 = context.wtf('le2')
cert3 = context.wtf('le3')
cert_path1 = join(context.config_dir, 'live/{0}/cert.pem'.format(cert1))
key_path2 = join(context.config_dir, 'live/{0}/privkey.pem'.format(cert2))
cert_path2 = join(context.config_dir, 'live/{0}/cert.pem'.format(cert2))
context.common(['-d', cert1])
context.common(['-d', cert2])
context.common(['-d', cert3])
context.common(['revoke', '--cert-path', cert_path1,
'--reason', 'cessationOfOperation'])
context.common(['revoke', '--cert-path', cert_path2, '--key-path', key_path2,
'--reason', 'keyCompromise'])
context.common(['unregister'])
output = context.common(['certificates'])
assert cert1 not in output
assert cert2 not in output
assert cert3 in output
def test_revoke_corner_cases(context):
cert1 = context.wtf('le1')
context.common(['-d', cert1])
with pytest.raises(subprocess.CalledProcessError) as error:
context.common([
'revoke', '--cert-name', cert1,
'--cert-path', join(context.config_dir, 'live/{0}/fullchain.pem'.format(cert1))
])
assert 'Exactly one of --cert-path or --cert-name must be specified' in error.out
assert os.path.isfile(join(context.config_dir, 'renewal/{0}.conf'.format(cert1)))
cert2 = context.wtf('le2')
context.common(['-d', cert2])
with open(join(context.config_dir, 'renewal/{0}.conf'.format(cert2)), 'r') as file:
data = file.read()
data = re.sub('archive_dir = .*{0}'.format(os.linesep),
'archive_dir = {0}{1}'.format(os.path.normpath(
join(context.config_dir, 'archive/{0}'.format(cert1))), os.linesep),
data)
with open(join(context.config_dir, 'renewal/{0}.conf'.format(cert2)), 'w') as file:
file.write(data)
output = context.common([
'revoke', '--cert-path', join(context.config_dir, 'live/{0}/cert.pem'.format(cert1))
])
assert 'Not deleting revoked certs due to overlapping archive dirs' in output
def test_wildcard_certificates(context):
if context.acme_server == 'boulder-v1':
pytest.skip('Wildcard certificates are not supported on ACME v1')
certname = context.wtf('wild')
context.common([
'-a', 'manual', '-d', '*.{0},{0}'.format(certname),
'--preferred-challenge', 'dns',
'--manual-auth-hook', context.manual_dns_auth_hook,
'--manual-cleanup-hook', context.manual_dns_cleanup_hook
])
assert exists(join(context.config_dir, 'live/{0}/fullchain.pem'.format(certname)))
def test_ocsp_status(context):
if context.acme_server == 'pebble':
pytest.skip('Pebble does not support OCSP status requests.')
# OCSP 1: Check stale OCSP status
sample_data_path = misc.load_sample_data_path(context.workspace)
output = context.common(['certificates', '--config-dir', sample_data_path])
assert output.count('TEST_CERT') == 2, ('Did not find two test certs as expected ({0})'
.format(output.count('TEST_CERT')))
assert output.count('EXPIRED') == 2, ('Did not find two expired certs as expected ({0})'
.format(output.count('EXPIRED')))
# OSCP 2: Check live certificate OCSP status (VALID)
output = context.common(['--domains', 'le-ocsp-check.wtf'])
assert output.count('VALID') == 1, 'Expected le-ocsp-check.wtf to be VALID'
assert output.count('EXPIRED') == 0, 'Did not expect le-ocsp-check.wtf to be EXPIRED'
# OSCP 3: Check live certificate OCSP status (REVOKED)
output = context.common(['certificates'])
assert output.count('INVALID') == 1, 'Expected le-ocsp-check.wtf to be INVALID'
assert output.count('REVOKED') == 1, 'Expected le-ocsp-check.wtf to be REVOKED'
@@ -2,10 +2,20 @@
Misc module contains stateless functions that could be used during pytest execution,
or outside during setup/teardown of the integration tests environment.
"""
import subprocess
import os
import time
import contextlib
import tempfile
import shutil
import multiprocessing
import sys
import stat
import errno
from distutils.version import LooseVersion
from six.moves import socketserver, SimpleHTTPServer
from OpenSSL import crypto
import requests
@@ -30,16 +40,142 @@ def check_until_timeout(url):
raise ValueError('Error, url did not respond after 150 attempts: {0}'.format(url))
def find_certbot_root_directory():
script_path = os.path.realpath(__file__)
current_dir = os.path.dirname(script_path)
while '.git' not in os.listdir(current_dir) and current_dir != os.path.dirname(current_dir):
current_dir = os.path.dirname(current_dir)
dirs = os.listdir(current_dir)
if '.git' not in dirs:
raise ValueError('Error, could not find certbot sources root directory')
return current_dir
def generate_csr(domains, key_path, csr_path, key_type='RSA'):
certbot_root_directory = find_certbot_root_directory()
script_path = os.path.join(certbot_root_directory, 'examples', 'generate-csr.py')
command = [
sys.executable, script_path, '--key-path', key_path, '--csr-path', csr_path,
'--key-type', key_type
]
command.extend(domains)
subprocess.check_call(command)
def load_sample_data_path(workspace):
certbot_root_directory = find_certbot_root_directory()
original = os.path.join(certbot_root_directory, 'tests', 'integration', 'sample-config')
copied = os.path.join(workspace, 'sample-config')
shutil.copytree(original, copied, symlinks=True)
return copied
def read_certificate(cert_path):
with open(cert_path, 'r') as file:
data = file.read()
cert = crypto.load_certificate(crypto.FILETYPE_PEM, data.encode('utf-8'))
return crypto.dump_certificate(crypto.FILETYPE_TEXT, cert).decode('utf-8')
class GraceFullTCPServer(socketserver.TCPServer):
allow_reuse_address = True
@contextlib.contextmanager
def execute_in_given_cwd(cwd):
"""
Context manager that will execute any command in the given cwd after entering context,
and restore current cwd when context is destroyed.
:param str cwd: the path to use as the temporary current workspace for python execution
"""
def create_tcp_server(port):
current_cwd = os.getcwd()
webroot = tempfile.mkdtemp()
def run():
GraceFullTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever()
process = multiprocessing.Process(target=run)
try:
os.chdir(cwd)
yield
try:
os.chdir(webroot)
process.start()
finally:
os.chdir(current_cwd)
check_until_timeout('http://localhost:{0}/'.format(port))
yield webroot
finally:
os.chdir(current_cwd)
try:
if process.is_alive():
process.terminate()
process.join() # Block until process is effectively terminated
finally:
shutil.rmtree(webroot)
def list_renewal_hooks_dirs(config_dir):
renewal_hooks_root = os.path.join(config_dir, 'renewal-hooks')
return [os.path.join(renewal_hooks_root, item) for item in ['pre', 'deploy', 'post']]
def generate_test_file_hooks(config_dir, hook_probe):
if sys.platform == 'win32':
extension = 'bat'
else:
extension = 'sh'
renewal_hooks_dirs = list_renewal_hooks_dirs(config_dir)
for hook_dir in renewal_hooks_dirs:
try:
os.makedirs(hook_dir)
except OSError as error:
if error.errno != errno.EEXIST:
raise
hook_path = os.path.join(hook_dir, 'hook.{0}'.format(extension))
if extension == 'sh':
data = '''\
#!/bin/bash -xe
if [ "$0" == "{0}" ]; then
if [ -z "$RENEWED_DOMAINS" -o -z "$RENEWED_LINEAGE" ]; then
echo "Environment variables not properly set!" >&2
exit 1
fi
fi
echo $(basename $(dirname "$0")) >> "{1}"\
'''.format(hook_path, hook_probe)
else:
data = '''\
'''
with open(hook_path, 'w') as file:
file.write(data)
os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IEXEC)
def manual_http_hooks(http_server_root):
auth = (
'{0} -c "import os; '
"challenge_dir = os.path.join('{1}', '.well-known/acme-challenge'); "
'os.makedirs(challenge_dir); '
"challenge_file = os.path.join(challenge_dir, os.environ.get('CERTBOT_TOKEN')); "
"open(challenge_file, 'w').write(os.environ.get('CERTBOT_VALIDATION')); "
'"'
).format(sys.executable, http_server_root)
cleanup = (
'{0} -c "import os; import shutil; '
"well_known = os.path.join('{1}', '.well-known'); "
'shutil.rmtree(well_known); '
'"'
).format(sys.executable, http_server_root)
return auth, cleanup
def get_certbot_version():
output = subprocess.check_output(['certbot', '--version'], universal_newlines=True)
# Typical response is: output = 'certbot 0.31.0.dev0'
# version_str = output.split(' ')[1]
return LooseVersion('0.31.0.dev0')
+4
View File
@@ -8,8 +8,12 @@ install_requires = [
'pytest',
'pytest-cov',
'pytest-xdist',
'pytest-rerunfailures==4.2',
'pytest-sugar',
'coverage',
'six',
'pyopenssl',
'cryptography',
'requests',
'pyyaml',
]
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This script generates a simple SAN CSR to be used with Let's Encrypt
# CA. Mostly intended for "auth --csr" testing, but, since it's easily
# auditable, feel free to adjust it and use it on your production web
# server.
import os
import argparse
from OpenSSL import crypto
try:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
cryptography=True
except ImportError:
cryptography=False
CWD = os.getcwd()
def generate_parser():
one_parser = argparse.ArgumentParser(description='Generate a simple SAN CSR')
one_parser.add_argument('domain', nargs='+', help='A FQDN to add in the CSR.')
one_parser.add_argument('--key-path', default=os.path.join(CWD, 'key.pem'),
help='Path for the generated key '
'(default is key.pem in current directory)')
one_parser.add_argument('--csr-path', default=os.path.join(CWD, 'csr.der'),
help='Path for the generated certificate '
'(default is csr.der in current directory)')
one_parser.add_argument('--key-type', default='RSA',
choices=['RSA', 'ECDSA'],
help='Key type to use, ECDSA is supported only '
'if cryptography module is available')
return one_parser
def main(domains, key_path, csr_path, key_type):
san = ', '.join(['DNS: {0}'.format(item) for item in domains])
if key_type == 'RSA':
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)
elif key_type == 'ECDSA':
if not cryptography:
raise ValueError('Error, cryptography module is not installed,'
'but is required to use ECDSA')
key = ec.generate_private_key(ec.SECP384R1(), default_backend())
key = key.private_bytes(encoding=Encoding.PEM, format=PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=NoEncryption())
key = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
else:
raise ValueError('Invalid key type: {0}'.format(key_type))
with open(key_path, 'wb') as file:
file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
req = crypto.X509Req()
san_constraint = crypto.X509Extension(b'subjectAltName', False, san.encode('utf-8'))
req.add_extensions([san_constraint])
req.set_pubkey(key)
req.sign(key, 'sha256')
with open(csr_path, 'wb') as file:
file.write(crypto.dump_certificate_request(crypto.FILETYPE_ASN1, req))
print('You can now run: certbot auth --csr {0}'.format(csr_path))
if __name__ == '__main__':
parser = generate_parser()
namespace = parser.parse_args()
main(namespace.domain, namespace.key_path, namespace.csr_path, namespace.key_type)
+1
View File
@@ -52,6 +52,7 @@ pytest-cov==2.5.1
pytest-forked==0.2
pytest-xdist==1.22.5
pytest-sugar==0.9.2
pytest-rerunfailures==4.2
python-dateutil==2.6.1
python-digitalocean==1.11
PyYAML==3.13