Verified Commit ed206f6f authored by Štěpán Kotek's avatar Štěpán Kotek Committed by Petr Špaček

deckard: Put trust anchors for each domain into separate files

RFC 5011 implementation in kresd and Unbound require separate
trust anchor file for each domain.
parent a6e21a87
Pipeline #13444 passed with stage
in 1 minute and 30 seconds
#!/usr/bin/env python
import argparse
from datetime import datetime
import errno
import logging
import logging.config
import os
......@@ -19,6 +20,8 @@ from pydnstest import scenario, testserver, test
# path to Deckard files
INSTALLDIR = os.path.dirname(os.path.abspath(__file__))
# relative to working directory
TRUST_ANCHOR_SUBDIR = 'ta'
class IfaceManager(object):
......@@ -190,7 +193,33 @@ def _fixme_prebind_hack(sockfamily, childaddr):
sock.listen(5)
def setup_daemon_files(prog_cfg, template_ctx):
def create_trust_anchor_files(ta_files, work_dir):
"""
Write trust anchor files in specified working directory.
Params:
ta_files Dict {domain name: [TA lines]}
Returns:
List of absolute filesystem paths to TA files.
"""
full_paths = []
for domain, ta_lines in ta_files.items():
file_name = u'{}.key'.format(domain)
full_path = os.path.realpath(
os.path.join(work_dir, TRUST_ANCHOR_SUBDIR, file_name))
full_paths.append(full_path)
dir_path = os.path.dirname(full_path)
try:
os.makedirs(dir_path)
except OSError as ex:
if ex.errno != errno.EEXIST:
raise
with open(full_path, "w") as ta_file:
ta_file.writelines('{0}\n'.format(l) for l in ta_lines)
return full_paths
def setup_daemon_files(prog_cfg, template_ctx, ta_files):
name = prog_cfg['name']
# add program-specific variables
subst = template_ctx.copy()
......@@ -198,9 +227,12 @@ def setup_daemon_files(prog_cfg, template_ctx):
subst['WORKING_DIR'] = prog_cfg['dir']
os.mkdir(prog_cfg['dir'])
subst['SELF_ADDR'] = prog_cfg['ipaddr']
# daemons might write to TA files so every daemon gets its own copy
subst['TRUST_ANCHOR_FILES'] = create_trust_anchor_files(
ta_files, prog_cfg['dir'])
# generate configuration files
j2template_loader = jinja2.FileSystemLoader(
searchpath=os.path.dirname(os.path.abspath(__file__)))
......@@ -259,7 +291,7 @@ def process_file(path, args, prog_cfgs):
"""Parse scenario from a file object and create workdir."""
# Parse scenario
case, cfg_text = scenario.parse_file(os.path.realpath(path))
cfg_ctx = scenario.parse_config(cfg_text, args.qmin, INSTALLDIR)
cfg_ctx, ta_files = scenario.parse_config(cfg_text, args.qmin, INSTALLDIR)
template_ctx = setup_network(cfg_ctx['_SOCKET_FAMILY'], prog_cfgs)
# merge variables from scenario with generated network variables (scenario has priority)
template_ctx.update(cfg_ctx)
......@@ -270,7 +302,7 @@ def process_file(path, args, prog_cfgs):
# get working directory and environment variables
tmpdir = setup_common_env(cfg_ctx)
try:
daemons = setup_daemons(tmpdir, prog_cfgs, template_ctx)
daemons = setup_daemons(tmpdir, prog_cfgs, template_ctx, ta_files)
run_testcase(daemons,
case,
template_ctx['ROOT_ADDR'],
......@@ -283,13 +315,13 @@ def process_file(path, args, prog_cfgs):
raise
def setup_daemons(tmpdir, prog_cfgs, template_ctx):
def setup_daemons(tmpdir, prog_cfgs, template_ctx, ta_files):
"""Configure daemons and run the test"""
# Setup daemon environment
daemons = []
for prog_cfg in prog_cfgs['programs']:
daemon_env = setup_daemon_env(prog_cfg, tmpdir)
setup_daemon_files(prog_cfg, template_ctx)
setup_daemon_files(prog_cfg, template_ctx, ta_files)
daemon_proc = run_daemon(prog_cfg, daemon_env)
daemons.append({'proc': daemon_proc, 'cfg': prog_cfg})
conncheck_daemon(daemon_proc, prog_cfg, template_ctx['_SOCKET_FAMILY'])
......@@ -327,6 +359,8 @@ def test_platform():
def deckard():
"""Entrypoint for script"""
# auxilitary classes for argparse
class ColonSplitter(argparse.Action): # pylint: disable=too-few-public-methods
"""Split argument string into list holding items separated by colon."""
......
......@@ -94,7 +94,7 @@ Commented default values taken from ``unbound_run.sh`` follow:
ADDITIONAL="-d -c unbound.conf"
# Template file names
TEMPLATE="template/unbound.j2:template/hints_zone.j2:template/unbound_dnssec.j2"
TEMPLATE="template/unbound.j2:template/hints_zone.j2"
# Config file names: generated respectively from templates above
CONFIG="unbound.conf:hints.zone:ta.keys"
......@@ -208,7 +208,7 @@ Custom templates can be used in the same way as templates listed in existing [ru
TESTS="sets/resolver" \
DAEMON="unbound" \
ADDITIONAL="-d -c unbound.conf" \
TEMPLATE="template/unbound.j2:template/hints_zone.j2:template/unbound_dnssec.j2" \
TEMPLATE="template/unbound.j2:template/hints_zone.j2" \
CONFIG="unbound.conf:hints.zone:ta.keys"
(These are the default values for Unbound.)
......@@ -309,7 +309,6 @@ The YAML file contains **ordered** list of binaries and their parameters. Deckar
templates:
- template/unbound.j2
- template/hints_zone.j2 # this template uses variable ROOT_ADDR
- template/unbound_dnssec.j2
configs:
- unbound.conf
- hints.zone
......
......@@ -2,7 +2,6 @@ from __future__ import absolute_import
import binascii
import calendar
from datetime import datetime
import errno
import logging
import os
......@@ -12,9 +11,11 @@ import socket
import string
import struct
import time
from datetime import datetime
import dns.dnssec
import dns.message
import dns.name
import dns.rcode
import dns.rrset
import dns.tsigkeyring
......@@ -874,12 +875,16 @@ def get_next(file_in, skip_empty=True):
def parse_config(scn_cfg, qmin, installdir):
"""
Transform scene config (key, value) pairs into dict filled with defaults.
Returns tuple:
context dict: {Jinja2 variable: value}
trust anchor dict: {domain: [TA lines for particular domain]}
"""
# defaults
do_not_query_localhost = True
harden_glue = True
sockfamily = 0 # auto-select value for socket.getaddrinfo
trust_anchor_list = []
trust_anchor_files = {}
stub_addr = None
override_timestamp = None
......@@ -896,7 +901,12 @@ def parse_config(scn_cfg, qmin, installdir):
if k == 'query-minimization':
qmin = str2bool(v)
elif k == 'trust-anchor':
trust_anchor_list.append(v.strip('"\''))
trust_anchor = v.strip('"\'')
trust_anchor_list.append(trust_anchor)
domain = dns.name.from_text(trust_anchor.split()[0]).canonicalize()
if domain not in trust_anchor_files:
trust_anchor_files[domain] = []
trust_anchor_files[domain].append(trust_anchor)
elif k == 'val-override-timestamp':
override_timestamp_str = v.strip('"\'')
override_timestamp = int(override_timestamp_str)
......@@ -948,6 +958,7 @@ def parse_config(scn_cfg, qmin, installdir):
"INSTALL_DIR": installdir,
"QMIN": str(qmin).lower(),
"TRUST_ANCHORS": trust_anchor_list,
"TRUST_ANCHOR_FILES": trust_anchor_files.keys()
}
if stub_addr:
ctx['ROOT_ADDR'] = stub_addr
......@@ -961,7 +972,7 @@ def parse_config(scn_cfg, qmin, installdir):
ctx['_SOCKET_FAMILY'] = sockfamily
if override_timestamp:
ctx['_OVERRIDE_TIMESTAMP'] = override_timestamp
return ctx
return (ctx, trust_anchor_files)
def parse_file(path):
......
""" This is unittest file for parse methods in scenario.py """
import os
from pydnstest.scenario import parse_config
def test_parse_config__trust_anchor():
"""Checks if trust-anchors are separated into files according to domain."""
anchor1 = u'domain1.com.\t3600\tIN\tDS\t11901 7 1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
anchor2 = u'domain2.net.\t3600\tIN\tDS\t59835 7 1 cccccccccccccccccccccccccccccccccccccccc'
anchor3 = u'domain1.com.\t3600\tIN\tDS\t11902 7 1 1111111111111111111111111111111111111111'
anchors = [[u'trust-anchor', u'"{}"'.format(anchor1)],
[u'trust-anchor', u'"{}"'.format(anchor2)],
[u'trust-anchor', u'"{}"'.format(anchor3)]]
args = (anchors, True, os.getcwd())
_, ta_files = parse_config(*args)
assert sorted(ta_files.values()) == sorted([[anchor1, anchor3], [anchor2]])
"""
Example of pytest.
"""
def test_true():
"""This test is always True, it is used so that empty suite don't fail."""
assert True
......@@ -22,8 +22,9 @@ mode('permissive')
{% endif %}
-- Always retry failing resolver
option('NO_THROTTLE', true)
{% for TA in TRUST_ANCHORS %}
trust_anchors.add('{{TA}}')
{% for TAF in TRUST_ANCHOR_FILES %}
trust_anchors.add_file('{{TAF}}')
{% endfor %}
{% if FEATURES.min_ttl is defined %}
......
......@@ -451,6 +451,10 @@ server:
# and under the terms of our LICENSE (see that file in the source).
# auto-trust-anchor-file: "/var/lib/unbound/root.key"
{% for TAF in TRUST_ANCHOR_FILES %}
auto-trust-anchor-file: "{{TAF}}"
{% endfor %}
# File with DLV trusted keys. Same format as trust-anchor-file.
# There can be only one DLV configured, it is trusted from root down.
# DLV is going to be decommissioned. Please do not use it any more.
......@@ -460,7 +464,6 @@ server:
# with several entries, one file per entry.
# Zone file format, with DS and DNSKEY entries.
# Note this gets out of date, use auto-trust-anchor-file please.
trust-anchor-file: "ta.keys"
# Trusted key for validation. DS or DNSKEY. specify the RR on a
# single line, surrounded by "". TTL is ignored. class is IN default.
......
""" This is unittest file for parse methods in scenario.py """
import os
import shutil
import tempfile
from deckard import create_trust_anchor_files
def test_create_trust_anchor_files():
"""Trust anchors must be into separate files grouped by domain."""
anchor1a = u'domain1.com.\t3600\tIN\tDS\t11901 7 1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
anchor1b = u'domain1.com.\t3600\tIN\tDS\t11902 7 1 1111111111111111111111111111111111111111'
anchor2a = u'domain2.net.\t3600\tIN\tDS\t59835 7 1 cccccccccccccccccccccccccccccccccccccccc'
trust_anchors = {'domain1.com': [anchor1a, anchor1b],
'domain2.net': [anchor2a]}
tmpdir = tempfile.mkdtemp()
try:
file_names = create_trust_anchor_files(trust_anchors, tmpdir)
assert sorted(file_names) == sorted('{wd}/ta/{f}'.format(wd=tmpdir, f=f)
for f in [u'domain1.com.key', u'domain2.net.key'])
for path in file_names:
with open(path) as ta_file:
file_name = os.path.basename(path)
assert file_name[-4:] == '.key'
domain = file_name[:-4]
assert ta_file.read() == ''.join(u'{}\n'.format(ta)
for ta in trust_anchors[domain])
finally:
shutil.rmtree(tmpdir)
......@@ -8,10 +8,10 @@ TESTS=${TESTS:-"sets/resolver"}
DAEMON=${DAEMON:-"unbound"}
# Template file name
TEMPLATE=${TEMPLATE:-"template/unbound.j2:template/hints_zone.j2:template/unbound_dnssec.j2"}
TEMPLATE=${TEMPLATE:-"template/unbound.j2:template/hints_zone.j2"}
# Config file name
CONFIG=${CONFIG:-"unbound.conf:hints.zone:ta.keys"}
CONFIG=${CONFIG:-"unbound.conf:hints.zone"}
# Additional parameter for unbound
# it means configuration file can be found in working directory
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment