Skip to content

Commit

Permalink
Get auth-block specific verifications + storages working
Browse files Browse the repository at this point in the history
Using special verification or storages by auth-block was not
tested and had a couple of implementation errors.

This commits adds basic tests for this feature, fixes the
issues and does some smaller features adjustments (e.g. if
there is only one storage or verification these are used as
default one).

Fix typos in dns01-dnsUpdate implementation.
  • Loading branch information
mswart committed Dec 13, 2016
1 parent 1418a18 commit 1c54bbe
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 63 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ domain=*.test.example.org

# CSR must also be signed by HMAC (via a the secret key)
[auth "mail-secure"]
# uise special verification and storage
verification = dns
storage = file
ip=198.51.100.21
hmac_type=sha256
hmac_key=A1YP67armNf3cBrecyJHdb035
Expand Down
38 changes: 29 additions & 9 deletions acmems/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(self, config=None):
self.blocks = []

def parse_block(self, name, options):
self.blocks.append(Block(name, options))
self.blocks.append(Block(name, options, self.config))

def process(self, client_address, headers, rfile):
return Processor(self, client_address, headers, rfile)
Expand Down Expand Up @@ -114,13 +114,13 @@ class Block():
''' One authentication block - combination of authentications
and list of allowed domains
'''
def __init__(self, name, options):
def __init__(self, name, options, config):
self.name = name
self.methods = []
self.domain_matchers = []
self.validator = None
self.storage = None
self.parse(options)
self.parse(options, config)

def possible(self, processor):
if not self.domain_matchers:
Expand All @@ -144,16 +144,26 @@ def check(self, processor):
return False
return True

def parse(self, options):
def parse(self, options, config):
unused_methods = [IPAuthMethod, AllAuthMethod, HmacAuthMethod]
for option, value in options:
if option == 'domain':
self.domain_matchers.append(value)
continue
if option == 'validator':
self.validator = value.strip()
if option == 'verification':
try:
self.validator = config.validators[value.strip()]
except KeyError:
from acmems.config import UnknownVerificationError
raise UnknownVerificationError('Validator "{}" undefined'.format(value.strip()))
continue
if option == 'storage':
self.storage = value.strip()
try:
self.storage = config.storages[value.strip()]
except KeyError:
from acmems.config import UnknownStorageError
raise UnknownStorageError('Storage "{}" undefined'.format(value.strip()))
continue
for method in self.methods:
if option in method.option_names:
method.parse(option, value)
Expand All @@ -170,6 +180,16 @@ def parse(self, options):
unused_methods.remove(method)
self.methods.append(method())
self.methods[-1].parse(option, value)
if self.validator is None:
self.validator = config.default_validator
if self.validator is False:
from acmems.config import UnknownVerificationError
raise UnknownVerificationError('auth "{}" does not define a validator and the default one is disabled'.format(self.name))
if self.storage is None:
self.storage = config.default_storage
if self.storage is False:
from acmems.config import UnknownStorageError
raise UnknownStorageError('auth "{}" does not define a storage and the default one is disabled'.format(self.name))


class Processor():
Expand Down Expand Up @@ -215,8 +235,8 @@ def acceptable(self):
# 3. final check
for block in possible_blocks:
if block.check(self):
self.validator = block.validator or self.auth.config.default_validator
self.storage = block.storage or self.auth.config.default_storage
self.validator = block.validator
self.storage = block.storage
return True
return False

Expand Down
21 changes: 12 additions & 9 deletions acmems/challenges.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
from datetime import datetime, timedelta
import json
import urllib.request
import base64
import warnings

import acme.client


from acmems.config import ConfigurationError
from acmems.config import ConfigurationError, UnusedOptionWarning
from acmems.server import ThreadedACMEServerByType, ACMEHTTPHandler
from acmems import exceptions


class ChallengeImplementor():
def __init__(self, type, options):
def __init__(self, type, name, options):
self.type = type
self.name = name
self.parse(options)


Expand Down Expand Up @@ -141,8 +141,11 @@ def parse(self, options):
self.dns_servers.append(value)
elif option == 'ttl':
self.ttl = int(value)
elif option == 'tiemout':
self.tiemout = int(value)
elif option == 'timeout':
self.timeout = int(value)
else:
warnings.warn('Option unknown [verification "{}"]{} = {}'.format(self.name, option, value),
UnusedOptionWarning, stacklevel=2)
if self.dns_servers is None:
self.dns_servers = ['127.0.0.1']
if self.ttl is None:
Expand All @@ -160,7 +163,7 @@ def add_entry(self, entry, value):
)
upd.add(entry, self.ttl, 'TXT', value)
try:
response = dns.query.tcp(upd, self.dns_server, timeout=self.timeout)
response = dns.query.tcp(upd, self.dns_servers[0], timeout=self.timeout)
rcode = response.rcode()
if rcode != dns.rcode.NOERROR:
rcode_text = dns.rcode.to_text(rcode)
Expand All @@ -181,8 +184,8 @@ def select_zone(self, entry):
}


def setup(type, options):
def setup(type, name, options):
try:
return implementors[type](type, options)
return implementors[type](type, name, options)
except KeyError:
raise ConfigurationError('Unsupported challenge type "{}"'.format(type))
51 changes: 35 additions & 16 deletions acmems/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ class MissingSectionError(ConfigurationError):
pass


class UnknownVerificationError(ConfigurationError):
pass


class UnknownStorageError(ConfigurationError):
pass


class SingletonOptionRedifined(ConfigurationError):
def __init__(self, section, option, old, new):
self.section = section
Expand Down Expand Up @@ -70,21 +78,27 @@ def parse(self, config):
self.parse_account_config(config.pop('account'))
self.parser_mgmt_config(config.pop('mgmt'))
special_group_re = re.compile('^(?P<type>(auth|verification|storage)) (?P<opener>"?)(?P<name>.+)(?P=opener)$')
auth_blocks = []
for group, options in config.items():
match = special_group_re.match(group)
if match:
if match.group('type') == 'auth':
self.auth.parse_block(match.group('name'), options)
# parse auth blocks last to have verification and storage blocks processed
auth_blocks.append((match.group('name'), options))
elif match.group('type') == 'verification':
self.parse_verification_group(match.group('name'), options)
else:
self.parse_storage_group(match.group('name'), options)
else:
warnings.warn('Unknown section name: {0}'.format(group),
UnusedSectionWarning, stacklevel=2)

self.setup_default_validator()
self.setup_default_storage()

for name, options in auth_blocks:
self.auth.parse_block(name, options)

@staticmethod
def read_data(config):
""" Reads the given file name. It assumes that the file has a INI file
Expand All @@ -102,7 +116,7 @@ def read_data(config):
for line in f:
line = line.strip()
# ignore comments:
if line.startswith('#'):
if line.startswith('#') or line.startswith(';'):
continue
if not line:
continue
Expand Down Expand Up @@ -186,29 +200,34 @@ def parse_verification_group(self, name, options):
if option != 'type':
raise ConfigurationError('A verification must start with the type value!')
from acmems.challenges import setup
self.validators[name] = setup(value, options)
self.validators[name] = setup(value, name, options)

def setup_default_validator(self):
if self.default_validator is None:
self.default_validator = 'http'
if not self.validators:
from acmems.challenges import setup
self.validators['http'] = setup('http01', ())
self.validators['dns01-boulder'] = setup('dns01-boulder', ())
if self.default_validator is not False:
if self.default_validator is False: # default validator disabled
return
if self.default_validator: # defined
self.default_validator = self.validators[self.default_validator]
return
if len(self.validators) == 1: # we use the only defined validator as default
self.default_validator = list(self.validators.values())[0]
else: # define a default http storage
from acmems.challenges import setup
self.default_validator = self.validators['http'] = setup('http01', 'http', ())

def parse_storage_group(self, name, options):
option, value = options.pop(0)
if option != 'type':
raise ConfigurationError('A storage must start with the type value!')
from acmems.storages import setup
self.storages[name] = setup(value, options)
self.storages[name] = setup(value, name, options)

def setup_default_storage(self):
if self.default_storage is None:
self.default_storage = 'none'
from acmems.storages import setup
self.storages[self.default_storage] = setup('none', ())
if self.default_storage is not False:
if self.default_storage is False: # default storage disabled
return
if self.default_storage:
self.default_storage = self.storages[self.default_storage]
if len(self.storages) == 1:
self.default_storage = list(self.storages.values())[0]
else:
from acmems.storages import setup
self.default_storage = self.storages['none'] = setup('none', 'none', ())
7 changes: 4 additions & 3 deletions acmems/storages.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@


class StorageImplementor():
def __init__(self, type, options):
def __init__(self, type, name, options):
self.type = type
self.name = name
self.parse(options)


Expand Down Expand Up @@ -80,8 +81,8 @@ def add_to_cache(self, csr, cert):
}


def setup(type, options):
def setup(type, name, options):
try:
return implementors[type](type, options)
return implementors[type](type, name, options)
except KeyError:
raise ConfigurationError('Unsupported storage type "{}"'.format(type))
8 changes: 8 additions & 0 deletions configs/integration.ini
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ hmac_type=sha256
hmac_key=n048aX0G2Gc8zAvUfcbz8fFIpRwi1D
domain=integration*.org
domain=www.integration*.org

[auth "dns"]
ip=127.0.0.1
hmac_type=sha256
hmac_key=imR32v5KFTVJ03jKhvggJygRvz8Ev2
verification=boulder
domain=dnsintegration*.org
domain=www.dnsintegration*.org
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def ckey():

@pytest.fixture(scope='session')
def http_server(request):
validator = challenges.setup('http01', (('listener', '127.0.0.1:5002'),))
validator = challenges.setup('http01', 'http', (('listener', '127.0.0.1:5002'),))
services = validator.start()

def fin():
Expand All @@ -55,7 +55,7 @@ def fin():

@pytest.fixture(scope='session')
def dnsboulder_validator(request):
validator = challenges.setup('dns01-boulder', ())
validator = challenges.setup('dns01-boulder', 'dns', ())
validator.start()
return validator

Expand Down
2 changes: 2 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ def M(configcontent, connect=False, validator=None):
c = config.Configurator(io.StringIO(configcontent))
if validator:
c.default_validator = validator
for block in c.auth.blocks:
block.validator = validator
return manager.ACMEManager(c, connect=connect)


Expand Down
2 changes: 2 additions & 0 deletions tests/integration/gencert.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@
subprocess.check_call(['openssl', 'x509', '-in', 'tests/integration/work/domain-201512.pem', '-noout', '-text'])

subprocess.check_call(['cmp', 'tests/integration/work/domain-201512.pem', 'tests/integration/work/domain-201512-2.pem'])

subprocess.check_call(['openssl', 'x509', '-in', 'tests/integration/work/dns-201512.pem', '-noout', '-text'])
2 changes: 2 additions & 0 deletions tests/integration/gencert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ rm -f tests/integration/work/*
openssl genrsa 4096 > tests/integration/work/domain.key

openssl req -new -sha256 -key tests/integration/work/domain.key -subj "/CN=integration$$.org" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:integration$$.org,DNS:www.integration$$.org")) > tests/integration/work/domain-201512.csr
openssl req -new -sha256 -key tests/integration/work/domain.key -subj "/CN=dnsintegration$$.org" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:dnsintegration$$.org,DNS:www.dnsintegration$$.org")) > tests/integration/work/dns-201512.csr

wget --post-file=tests/integration/work/domain-201512.csr --header="`openssl dgst -sha256 -hmac 'n048aX0G2Gc8zAvUfcbz8fFIpRwi1D' tests/integration/work/domain-201512.csr | sed -e 's/HMAC-\(.*\)(.*)= *\(.*\)/Authentication: hmac name=\L\1\E, hash=\2/'`" http://127.0.0.1:1313/sign -O tests/integration/work/domain-201512.pem
wget --post-file=tests/integration/work/domain-201512.csr --header="`openssl dgst -sha256 -hmac 'n048aX0G2Gc8zAvUfcbz8fFIpRwi1D' tests/integration/work/domain-201512.csr | sed -e 's/HMAC-\(.*\)(.*)= *\(.*\)/Authentication: hmac name=\L\1\E, hash=\2/'`" http://127.0.0.1:1313/sign -O tests/integration/work/domain-201512-2.pem
wget --post-file=tests/integration/work/dns-201512.csr --header="`openssl dgst -sha256 -hmac 'imR32v5KFTVJ03jKhvggJygRvz8Ev2' tests/integration/work/dns-201512.csr | sed -e 's/HMAC-\(.*\)(.*)= *\(.*\)/Authentication: hmac name=\L\1\E, hash=\2/'`" http://127.0.0.1:1313/sign -O tests/integration/work/dns-201512.pem
47 changes: 45 additions & 2 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from acmems import auth, config
from acmems import auth, config, challenges, storages
from tests.helpers import gencsrpem


Expand All @@ -28,7 +28,8 @@ def test_reject_for_no_auth_block():
with a.process('192.0.2.34', {}, '') as p:
assert p.acceptable() is False

# generall

# general


def test_warning_on_unknown_option(a, ckey):
Expand All @@ -39,6 +40,48 @@ def test_warning_on_unknown_option(a, ckey):
assert '192.0.2.34' in str(e[0].message)


def test_error_on_unknown_verification(a, ckey):
with pytest.raises(config.UnknownVerificationError) as e:
a.parse_block('err', [('verification', 'asdfasfd'), ('domain', 'example.org')])
assert 'asdfasfd' in str(e)


def test_error_on_unknown_storage(a, ckey):
with pytest.raises(config.UnknownStorageError) as e:
a.parse_block('err', [('storage', 'asdfasfd'), ('domain', 'example.org')])
assert 'asdfasfd' in str(e)


def test_error_on_no_verification_and_disabled_default_verification(a, ckey):
a.config.default_validator = False
with pytest.raises(config.UnknownVerificationError) as e:
a.parse_block('err', [('domain', 'example.org')])
assert 'auth "err"' in str(e)
assert 'default' in str(e)
assert 'disabled' in str(e)


def test_error_on_no_storage_and_disabled_default_one(a, ckey):
a.config.default_storage = False
with pytest.raises(config.UnknownStorageError) as e:
a.parse_block('err', [('domain', 'example.org')])
assert 'auth "err"' in str(e)
assert 'default' in str(e)
assert 'disabled' in str(e)


def test_different_validator(a, ckey):
a.config.validators['bob'] = bob = challenges.setup('dns01-dnsUpdate', 'bob', ())
a.parse_block('err', [('verification', 'bob'), ('domain', 'example.org')])
assert a.blocks[0].validator is bob


def test_different_storage(a, ckey):
a.config.storages['bob'] = bob = storages.setup('none', 'bob', ())
a.parse_block('err', [('storage', 'bob'), ('domain', 'example.org')])
assert a.blocks[0].storage is bob


# all auth


Expand Down

0 comments on commit 1c54bbe

Please sign in to comment.