diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 43d3d432f0074c3c49b9f409e67d01192336c446..a2cc0f946aae537f3a31fa4ce93c935e0729157d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -302,7 +302,7 @@ root.hints: - scripts/update-root-hints.sh test:valgrind: - <<: *test + <<: *test_flaky # lost block in /bin/bash during ta_update when: delayed start_in: '30 seconds' script: diff --git a/.luacheckrc b/.luacheckrc index 1729bd4b5ca18851f0d4c7d208438be09072cee7..885fa82205c52ec64a809f275423d5d325387a32 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,22 +1,22 @@ std = 'luajit' new_read_globals = { + 'cache', + 'event', 'help', - 'quit', + '_hint_root_file', 'hostname', + 'map', + 'modules', + 'net', 'package_version', - 'user', - 'verbose', + 'quit', 'resolve', - 'tojson', + 'ta_update', 'todname', - 'map', - 'net', - 'cache', - 'modules', - 'trust_anchors', + 'tojson', + 'user', + 'verbose', 'worker', - 'event', - '_hint_root_file', -- Sandbox declarations 'kB', 'MB', @@ -31,7 +31,6 @@ new_read_globals = { 'warn', 'log', 'mode', - 'trust_anchors', 'reorder_RR', 'option', 'env', @@ -72,10 +71,10 @@ ignore = { } -- Sandbox can set global variables -files['daemon/lua'].ignore = {'111', '121', '122'} -files['daemon/lua/kres-gen.lua'].ignore = {'631'} -- Allow overly long lines +files['**/daemon/lua'].ignore = {'111', '121', '122'} +files['**/daemon/lua/kres-gen.lua'].ignore = {'631'} -- Allow overly long lines -- Tests and scripts can use global variables files['scripts'].ignore = {'111', '112', '113'} files['tests'].ignore = {'111', '112', '113'} -files['modules/**/*.test.lua'].ignore = {'111', '112', '113', '121', '122'} -files['daemon/**/*.test.lua'].ignore = {'111', '112', '113', '121', '122'} +files['**/modules/**/*.test.lua'].ignore = {'111', '112', '113', '121', '122'} +files['**/daemon/**/*.test.lua'].ignore = {'111', '112', '113', '121', '122'} diff --git a/NEWS b/NEWS index a82954d00a82058c31a55ddbc3c382e3dd214529..f6a02544163c83d90ea354c1f1f035a1f33f98a0 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,9 @@ Incompatible changes -------------------- - see upgrading guide: https://knot-resolver.readthedocs.io/en/v4.0.0/upgrading.html#upgrade-from-3-to-4 +- configuration: trust_anchors aliases .file, .config() and .negative were removed (!788) +- configuration: trust_anchors.keyfile_default is no longer accessible (!788) +- daemon: -k/--keyfile and -K/--keyfile-ro options were removed - meson build system is now used for builds (!771) - build with embedded LMBD is no longer supported - default modules dir location has changed @@ -33,6 +36,7 @@ Bugfixes - policy.RPZ: log problems from zone-file level of parser as well (#453) - fix flushing of messages to logs in some cases (!781) - fix fallback when SERVFAIL or REFUSED is received from upstream (!784) +- fix crash when dealing with unknown TA key algorhitm (#449) Module API changes ------------------ diff --git a/ci/respdiff/kresd.config b/ci/respdiff/kresd.config index c733601a06794ccf30eb0292e4ed3732ac69d083..fb5a33d29c0be2963cdb5f2d0b92600c9038af9c 100644 --- a/ci/respdiff/kresd.config +++ b/ci/respdiff/kresd.config @@ -5,7 +5,7 @@ net.listen('127.0.0.1', 8853, { tls = true }) net.ipv6=false -- Auto-maintain root TA -trust_anchors.file = '.local/etc/knot-resolver/root.keys' +trust_anchors.add_file('.local/etc/knot-resolver/root.keys') -- Large cache size, so we don't need to flush often -- This can be larger than available RAM, least frequently accessed diff --git a/daemon/README.rst b/daemon/README.rst index 2bcd35bd92c4fa0e7399b7d5c45d6404dfbcc9b2..3d76095bd2a4dfc0cb99be9bec71a5e09bbc23ef 100644 --- a/daemon/README.rst +++ b/daemon/README.rst @@ -305,7 +305,7 @@ Environment net = { '127.0.0.1', '::1' } -- unprivileged cache.size = 100*MB - trust_anchors.file = 'root.key' + trust_anchors.add_file('root.key') Example output: @@ -391,14 +391,14 @@ add the following snippet to your configuration file. .. code-block:: lua -- turns off DNSSEC validation - trust_anchors.keyfile_default = nil + trust_anchors.remove('.') The resolver supports DNSSEC including :rfc:`5011` automated DNSSEC TA updates and :rfc:`7646` negative trust anchors. Depending on your distribution, DNSSEC trust anchors should be either maintained in accordance with the distro-wide policy, or automatically maintained by the resolver itself. -.. function:: trust_anchors.add_file(keyfile, readonly) +.. function:: trust_anchors.add_file(keyfile[, readonly = false]) :param string keyfile: path to the file. :param readonly: if true, do not attempt to update the file. @@ -421,21 +421,22 @@ policy, or automatically maintained by the resolver itself. [ ta ] key: 19036 state: Valid -.. function:: trust_anchors.config(keyfile, readonly) +.. function:: trust_anchors.remove(zonename) - Alias for `add_file`. It is also equivalent to CLI parameter ``-k <keyfile>`` - and ``trust_anchors.file = keyfile``. + Remove specified trust anchor from trusted key set. Removing trust anchor for the root zone effectivelly disables DNSSEC validation (unless you configured another trust anchor). -.. envvar:: trust_anchors.keyfile_default = keyfile_default + .. code-block:: lua + + > trust_anchors.remove('.') + true - Set by ``keyfile_default`` option during compilation. This can be explicitly - set to ``nil`` to disable DNSSEC validation. + If you want to disable DNSSEC validation for a particular domain but keep it enabled for the rest of DNS tree, use :func:`trust_anchors.set_insecure`. .. envvar:: trust_anchors.hold_down_time = 30 * day :return: int (default: 30 * day) - Modify RFC5011 hold-down timer to given value. Example: ``30 * sec`` + Modify RFC5011 hold-down timer to given value. Intended only for testing purposes. Example: ``30 * sec`` .. envvar:: trust_anchors.refresh_time = nil @@ -443,6 +444,7 @@ policy, or automatically maintained by the resolver itself. Modify RFC5011 refresh timer to given value (not set by default), this will force trust anchors to be updated every N seconds periodically instead of relying on RFC5011 logic and TTLs. + Intended only for testing purposes. Example: ``10 * sec`` .. envvar:: trust_anchors.keep_removed = 0 @@ -457,16 +459,15 @@ policy, or automatically maintained by the resolver itself. :param table nta_list: List of domain names (text format) representing NTAs. - When you use a domain name as an NTA, DNSSEC validation will be turned off at/below these names. + When you use a domain name as an *negative trust anchor* (NTA), DNSSEC validation will be turned off at/below these names. Each function call replaces the previous NTA set. You can find the current active set in ``trust_anchors.insecure`` variable. - - .. tip:: Use the `trust_anchors.negative = {}` alias for easier configuration. + If you want to disable DNSSEC validation completely use :func:`trust_anchors.remove` function instead. Example output: .. code-block:: lua - > trust_anchors.negative = { 'bad.boy', 'example.com' } + > trust_anchors.set_insecure({ 'bad.boy', 'example.com' }) > trust_anchors.insecure [1] => bad.boy [2] => example.com @@ -481,6 +482,8 @@ policy, or automatically maintained by the resolver itself. Inserts DS/DNSKEY record(s) into current keyset. These will not be managed or updated, use it only for testing or if you have a specific use case for not using a keyfile. + .. note:: Static keys are very error-prone and should not be used in production. Use :func:`trust_anchors.add_file` instead. + Example output: .. code-block:: lua @@ -631,7 +634,7 @@ Example: $ kresd-query.lua www.sub.nic.cz 'assert(kres.dname2str(req:resolved().zone_cut.name) == "nic.cz.")' && echo "yes" yes - $ kresd-query.lua -C 'trust_anchors.config("root.keys")' nic.cz 'assert(req:resolved().flags.DNSSEC_WANT)' + $ kresd-query.lua -C 'trust_anchors.add_file("root.keys")' nic.cz 'assert(req:resolved().flags.DNSSEC_WANT)' $ echo $? 0 diff --git a/daemon/bindings/impl.c b/daemon/bindings/impl.c index 2db2895d2ffa3499385941bd40121d4f4f559ff1..21a8d7876eea140287fa30b8999fcd189b614e5a 100644 --- a/daemon/bindings/impl.c +++ b/daemon/bindings/impl.c @@ -71,17 +71,17 @@ void kr_bindings_register(lua_State *L) void lua_error_p(lua_State *L, const char *fmt, ...) { + /* Add a stack trace and throw the result as a lua error. */ + luaL_traceback(L, L, "error occured here (config filename:lineno is at the bottom, if config is involved):", 0); /* Push formatted custom message, prepended with "ERROR: ". */ - lua_pushliteral(L, "ERROR: "); + lua_pushliteral(L, "\nERROR: "); { va_list args; va_start(args, fmt); lua_pushvfstring(L, fmt, args); va_end(args); } - lua_concat(L, 2); - /* Add a stack trace and throw the result as a lua error. */ - luaL_traceback(L, L, lua_tostring(L, -1), 0); + lua_concat(L, 3); lua_error(L); /* TODO: we might construct a little more friendly trace by using luaL_where(). * In particular, in case the error happens in a function that was called diff --git a/daemon/cache.test/clear.test.lua b/daemon/cache.test/clear.test.lua index e7e96fd81c8ae37cdc4bd828bd701799efae9647..ed13186dcd29e7dc0162be0d421da328f6510971 100644 --- a/daemon/cache.test/clear.test.lua +++ b/daemon/cache.test/clear.test.lua @@ -32,7 +32,7 @@ ev = event.after(0, function () return 1 end) -- Import fake root zone; avoid interference with configured keyfile_default. -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') trust_anchors.add('. IN DS 48409 8 2 3D63A0C25BCE86621DE63636F11B35B908EFE8E9381E0E3E9DEFD89EA952C27D') local function check_answer(desc, qname, qtype, expected_rcode) diff --git a/daemon/lua/config.lua.in b/daemon/lua/config.lua similarity index 75% rename from daemon/lua/config.lua.in rename to daemon/lua/config.lua index 26f756a5c8a495b8bf4cf57f518868c04242d982..66e9c35e88f6bc1854ebebffd48d9b5023a184cf 100644 --- a/daemon/lua/config.lua.in +++ b/daemon/lua/config.lua @@ -31,13 +31,3 @@ end if require('ffi').C.kr_zonecut_is_empty(kres.context().root_hints) then _hint_root_file() end - -if not trust_anchors.keysets['\0'] and trust_anchors.keyfile_default then - if io.open(trust_anchors.keyfile_default, 'r') then - trust_anchors.config(trust_anchors.keyfile_default, @unmanaged@) - else - panic("cannot open default trust anchor file:'%s'", - trust_anchors.keyfile_default - ) - end -end diff --git a/daemon/lua/meson.build b/daemon/lua/meson.build index d11bf7731a4f94d210f87faa7d0306ce3fc08bc5..b2e51e20aa85614941e0871e6583b173c18d5388 100644 --- a/daemon/lua/meson.build +++ b/daemon/lua/meson.build @@ -15,9 +15,10 @@ trust_anchors = configure_file( output: 'trust_anchors.lua', configuration: ta_config, ) -config_lua = configure_file( - input: 'config.lua.in', - output: 'config.lua', + +sandbox = configure_file( + input: 'sandbox.lua.in', + output: 'sandbox.lua', configuration: ta_config, ) @@ -27,10 +28,10 @@ run_target( # run manually to re-generate kres-gen.lua ) lua_src = [ - config_lua, + files('config.lua'), files('kres.lua'), files('kres-gen.lua'), - files('sandbox.lua'), + sandbox, trust_anchors, files('zonefile.lua'), ] diff --git a/daemon/lua/sandbox.lua b/daemon/lua/sandbox.lua.in similarity index 97% rename from daemon/lua/sandbox.lua rename to daemon/lua/sandbox.lua.in index 017e3a39936ce61df81ec218c6f80b77ca869e25..562d1d7dcdc47006c558774b3fdb8335d24d071d 100644 --- a/daemon/lua/sandbox.lua +++ b/daemon/lua/sandbox.lua.in @@ -1,3 +1,4 @@ +local debug = require('debug') local ffi = require('ffi') -- Units @@ -14,7 +15,9 @@ day = 24 * hour -- Logging function panic(fmt, ...) - error(string.format('error: '..fmt, ...)) + print(debug.traceback('error occured here (config filename:lineno is ' + .. 'at the bottom, if config is involved):', 2)) + error(string.format('ERROR: '.. fmt, ...), 0) end function warn(fmt, ...) io.stderr:write(string.format(fmt..'\n', ...)) @@ -316,6 +319,7 @@ end -- Load embedded modules trust_anchors = require('trust_anchors') +modules.load('ta_update') modules.load('ta_signal_query') modules.load('policy') modules.load('priming') @@ -324,6 +328,9 @@ modules.load('detect_time_jump') modules.load('ta_sentinel') modules.load('edns_keepalive') +-- Load keyfile_default +trust_anchors.add_file('@keyfile_default@', @unmanaged@) + -- Interactive command evaluation function eval_cmd(line, raw) -- Compatibility sandbox code loading diff --git a/daemon/lua/trust_anchors.lua.in b/daemon/lua/trust_anchors.lua.in index 2497fbcb8762c92b8841c26246bfbb52d9ea3b0f..331f15b4ff468f9f2e929019b5c56bcc4c7b0a07 100644 --- a/daemon/lua/trust_anchors.lua.in +++ b/daemon/lua/trust_anchors.lua.in @@ -5,6 +5,23 @@ local C = ffi.C local trust_anchors -- the public pseudo-module, exported as global variable +-- RFC5011 state table +local key_state = { + Start = 'Start', AddPend = 'AddPend', Valid = 'Valid', + Missing = 'Missing', Revoked = 'Revoked', Removed = 'Removed' +} + +local function upgrade_required(msg) + if msg then + msg = msg .. '\n' + else + msg = '' + end + panic('Configuration upgrade required: ' .. msg .. 'Please refer to ' .. + 'https://knot-resolver.readthedocs.io/en/stable/upgrading.html') +end + +-- TODO: Move bootstrap to a separate module or even its own binary -- Fetch over HTTPS with peert cert checked local function https_fetch(url, ca) local ssl_ok, https = pcall(require, 'ssl.https') @@ -14,11 +31,11 @@ local function https_fetch(url, ca) end local resp = {} local r, c = https.request{ - url = url, - cafile = ca, - verify = {'peer', 'fail_if_no_peer_cert' }, - protocol = 'tlsv1_2', - sink = ltn12.sink.table(resp), + url = url, + cafile = ca, + verify = {'peer', 'fail_if_no_peer_cert' }, + protocol = 'tlsv1_2', + sink = ltn12.sink.table(resp), } if r == nil then return r, c end return resp[1] @@ -43,8 +60,8 @@ local function keydigest_is_valid(valid_from, valid_until) local err = ffi.C.kr_strptime_diff( format, time_now, time2utc(valid_from), time_diff) if (err ~= nil) then - error(string.format('failed to process "validFrom" constraint: %s', - ffi.string(err))) + error(string.format('failed to process "validFrom" constraint: %s', + ffi.string(err))) end local from_ok = time_diff[0] > 0 @@ -55,7 +72,7 @@ local function keydigest_is_valid(valid_from, valid_until) format, time_now, time2utc(valid_until), time_diff) if (err ~= nil) then error(string.format('failed to process "validUntil" constraint: %s', - ffi.string(err))) + ffi.string(err))) end until_ok = time_diff[0] < 0 end @@ -73,19 +90,19 @@ local function parse_xml_keydigest(attrs, inside, output) local valid_attrs = {id = true, validFrom = true, validUntil = true} for key, _ in pairs(fields) do assert(valid_attrs[key], - string.format('unsupported KeyDigest attribute "%s" found in "%s"', - key, attrs)) + string.format('unsupported KeyDigest attribute "%s" found in "%s"', + key, attrs)) end _, n = string.gsub(inside, "<([%w]+).->([^<]+)</[%w]+>", function (k, v) fields[k] = v end) assert(n >= 1, string.format('error parsing KeyDigest XML elements from "%s"', - inside)) + inside)) local mandatory_elements = {'KeyTag', 'Algorithm', 'DigestType', 'Digest'} for _, key in ipairs(mandatory_elements) do assert(fields[key], string.format('mandatory element %s is missing in "%s"', - key, inside)) + key, inside)) end assert(n == 4, string.format('found %d elements but expected 4 in %s', n, inside)) table.insert(output, fields) -- append to list of parsed keydigests @@ -101,7 +118,7 @@ local function generate_ds(keydigests) rrset = rrset .. '\n' .. rr else log('[ ta ] skipping trust anchor "%s" ' .. - 'because it is outside of validity range', rr) + 'because it is outside of validity range', rr) end end return rrset @@ -113,8 +130,8 @@ local function assert_str_match(str, pattern, expected) count = count + 1 end assert(count == expected, - string.format('expected %d occurences of "%s" but got %d in "%s"', - expected, pattern, count, str)) + string.format('expected %d occurences of "%s" but got %d in "%s"', + expected, pattern, count, str)) end -- Fetch root anchors in XML over HTTPS, returning a zone-file-style string @@ -145,181 +162,21 @@ local function bootstrap(url, ca) return false, string.format('[ ta ] no valid trust anchors found at "%s"', url) end local msg = '[ ta ] Root trust anchors bootstrapped over https with pinned certificate.\n' - .. ' You SHOULD verify them manually against original source:\n' - .. ' https://www.iana.org/dnssec/files\n' - .. '[ ta ] Current root trust anchors are:' - .. rrset + .. ' You SHOULD verify them manually against original source:\n' + .. ' https://www.iana.org/dnssec/files\n' + .. '[ ta ] Bootstrapped root trust anchors are:' + .. rrset return rrset, msg end --- RFC5011 state table -local key_state = { - Start = 'Start', AddPend = 'AddPend', Valid = 'Valid', - Missing = 'Missing', Revoked = 'Revoked', Removed = 'Removed' -} - --- Find key in current keyset -local function ta_find(keyset, rr) - local rr_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata) - assert(rr_tag >= 0 and rr_tag <= 65535, string.format('invalid RR: %s: %s', - kres.rr2str(rr), ffi.string(C.knot_strerror(rr_tag)))) - for i, ta in ipairs(keyset) do - -- Match key owner and content - local ta_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata) - assert(ta_tag >= 0 and ta_tag <= 65535, string.format('invalid RR: %s: %s', - kres.rr2str(ta), ffi.string(C.knot_strerror(ta_tag)))) - if ta.owner == rr.owner then - if ta.type == rr.type then - if rr.type == kres.type.DNSKEY then - if C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) == 0 then - return ta - end - elseif rr.type == kres.type.DS and ta.rdata == rr.rdata then - return ta - end - -- DNSKEY superseding DS, inexact match - elseif rr.type == kres.type.DNSKEY and ta.type == kres.type.DS then - if ta.key_tag == rr_tag then - keyset[i] = rr -- Replace current DS - rr.state = ta.state - rr.key_tag = ta.key_tag - return rr - end - -- DS key matching DNSKEY, inexact match - elseif rr.type == kres.type.DS and ta.type == kres.type.DNSKEY then - if rr_tag == ta_tag then - return ta - end - end - end - end - return nil -end - --- Evaluate TA status of a RR according to RFC5011. The time is in seconds. -local function ta_present(keyset, rr, hold_down_time, force_valid) - if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then - return false -- Ignore - end - -- Find the key in current key set and check its status - local now = os.time() - local key_revoked = (rr.type == kres.type.DNSKEY) and C.kr_dnssec_key_revoked(rr.rdata) - local key_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata) - assert(key_tag >= 0 and key_tag <= 65535, string.format('invalid RR: %s: %s', - kres.rr2str(rr), ffi.string(C.knot_strerror(key_tag)))) - local ta = ta_find(keyset, rr) - if ta then - -- Key reappears (KeyPres) - if ta.state == key_state.Missing then - ta.state = key_state.Valid - ta.timer = nil - end - -- Key is revoked (RevBit) - if ta.state == key_state.Valid or ta.state == key_state.Missing then - if key_revoked then - ta.state = key_state.Revoked - ta.timer = now + hold_down_time - end - end - -- Remove hold-down timer expires (RemTime) - if ta.state == key_state.Revoked and os.difftime(ta.timer, now) <= 0 then - ta.state = key_state.Removed - ta.timer = nil - end - -- Add hold-down timer expires (AddTime) - if ta.state == key_state.AddPend and os.difftime(ta.timer, now) <= 0 then - ta.state = key_state.Valid - ta.timer = nil - end - if rr.state ~= key_state.Valid or verbose() then - log('[ ta ] key: ' .. key_tag .. ' state: '..ta.state) - end - return true - elseif not key_revoked then -- First time seen (NewKey) - rr.key_tag = key_tag - if force_valid then - rr.state = key_state.Valid - else - rr.state = key_state.AddPend - rr.timer = now + hold_down_time - end - if rr.state ~= key_state.Valid or verbose() then - log('[ ta ] key: ' .. key_tag .. ' state: '..rr.state) - end - table.insert(keyset, rr) - return true - end - return false -end - --- TA is missing in the new key set. The time is in seconds. -local function ta_missing(ta, hold_down_time) - -- Key is removed (KeyRem) - local keep_ta = true - local key_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata) - assert(key_tag >= 0 and key_tag <= 65535, string.format('invalid RR: %s: %s', - kres.rr2str(ta), ffi.string(C.knot_strerror(key_tag)))) - if ta.state == key_state.Valid then - ta.state = key_state.Missing - ta.timer = os.time() + hold_down_time - - -- Remove key that is missing for too long - elseif ta.state == key_state.Missing and os.difftime(ta.timer, os.time()) <= 0 then - ta.state = key_state.Removed - log('[ ta ] key: '..key_tag..' removed because missing for too long') - keep_ta = false - - -- Purge pending key - elseif ta.state == key_state.AddPend then - log('[ ta ] key: '..key_tag..' purging') - keep_ta = false - end - log('[ ta ] key: '..key_tag..' state: '..ta.state) - return keep_ta -end - -local active_refresh, update -- forwards - --- Plan an event for refreshing the root DNSKEYs and re-scheduling itself -local function refresh_plan(keyset, delay, is_initial) - local owner_str = kres.dname2str(keyset.owner) -- maybe fix converting back and forth? - keyset.refresh_ev = event.after(delay, function () - resolve(owner_str, kres.type.DNSKEY, kres.class.IN, 'NO_CACHE', - function (pkt) - -- Schedule itself with updated timeout - local delay_new = active_refresh(keyset, kres.pkt_t(pkt), is_initial) - delay_new = keyset.refresh_time or trust_anchors.refresh_time or delay_new - log('[ ta ] next refresh for ' .. owner_str .. ' in ' - .. delay_new/hour .. ' hours') - refresh_plan(keyset, delay_new) - end) - end) -end - --- Refresh the DNSKEYs from the packet, and return time to the next check. -active_refresh = function (keyset, pkt, is_initial) - local retry = true - if pkt:rcode() == kres.rcode.NOERROR then - local records = pkt:section(kres.section.ANSWER) - local new_keys = {} - for _, rr in ipairs(records) do - if rr.type == kres.type.DNSKEY then - table.insert(new_keys, rr) - end - end - update(keyset, new_keys, is_initial) - retry = false - else - warn('[ ta ] active refresh failed for ' .. kres.dname2str(keyset.owner) - .. ' with rcode: ' .. pkt:rcode()) - end - -- Calculate refresh/retry timer (RFC 5011, 2.3) - local min_ttl = retry and day or 15 * day - for _, rr in ipairs(keyset) do -- 10 or 50% of the original TTL - min_ttl = math.min(min_ttl, (retry and 100 or 500) * rr.ttl) - end - return math.max(hour, min_ttl) +local function bootstrap_write(rrstr, filename) + local fname_tmp = filename .. '.lock.' .. tostring(worker.pid); + local file = assert(io.open(fname_tmp, 'w')) + file:write(rrstr) + file:close() + assert(os.rename(fname_tmp, filename)) end +-- Bootstrap end -- Update ta.comment and return decorated line representing the RR -- This is meant to be in zone-file format. @@ -335,7 +192,9 @@ end -- Write keyset to a file. States and timers are stored in comments. local function keyset_write(keyset) - if not keyset.filename then return false end -- not to be persisted + if not keyset.managed then -- not to be persistent, this is an error! + panic('internal error: keyset_write called for an unmanaged TA') + end local fname_tmp = keyset.filename .. '.lock.' .. tostring(worker.pid); local file = assert(io.open(fname_tmp, 'w')) for i = 1, #keyset do @@ -428,7 +287,7 @@ local function keyset_read(path, str) for _, ta in ipairs(tas) do if ta.owner ~= owner then return nil, string.format("do not mix %s and %s TAs in single file/string", - kres.dname2str(ta.owner), kres.dname2str(owner)) + kres.dname2str(ta.owner), kres.dname2str(owner)) end end tas.owner = owner @@ -456,100 +315,41 @@ local function keyset_publish(keyset) end if count == 0 then warn('[ ta ] ERROR: no anchors are trusted for ' .. - kres.dname2str(keyset.owner) .. ' !') + kres.dname2str(keyset.owner) .. ' !') end return count > 0 and not has_error end - --- Update existing keyset; return true if successful. --- Param `is_initial` (bool): force .NewKey states to .Valid, i.e. init empty keyset. -update = function (keyset, new_keys, is_initial) - if not new_keys then return false end - - -- Filter TAs to be purged from the keyset (KeyRem), in three steps - -- 1: copy TAs to be kept to `keepset` - local hold_down = (keyset.hold_down_time or trust_anchors.hold_down_time) / 1000 - local keepset = {} - local keep_removed = keyset.keep_removed or trust_anchors.keep_removed - for _, ta in ipairs(keyset) do - local keep = true - if not ta_find(new_keys, ta) then - -- Ad-hoc: RFC 5011 doesn't mention removing a Missing key. - -- Let's do it after a very long period has elapsed. - keep = ta_missing(ta, hold_down * 4) - end - -- Purge removed keys - if ta.state == key_state.Removed then - if keep_removed > 0 then - keep_removed = keep_removed - 1 - else - keep = false - end +local function add_file(path, unmanaged) + local managed = not unmanaged + if managed then + if not ta_update then + panic('[ ta ] automatic update for ' .. path .. ' requested, ' + .. 'but required module ta_update is not loaded') end - if keep then - table.insert(keepset, ta) - end - end - -- 2: remove all TAs - other settings etc. will remain in the keyset - for i, _ in ipairs(keyset) do - keyset[i] = nil - end - -- 3: move TAs to be kept into the keyset (same indices) - for k, ta in pairs(keepset) do - keyset[k] = ta - end - - -- Evaluate new TAs - for _, rr in ipairs(new_keys) do - if (rr.type == kres.type.DNSKEY or rr.type == kres.type.DS) and rr.rdata ~= nil then - ta_present(keyset, rr, hold_down, is_initial) - end - end - - -- Store the keyset - keyset_write(keyset) - - -- Start using the new TAs. - if not keyset_publish(keyset) then - -- TODO: try to rebootstrap if for root? - return false - elseif verbose() then - log('[ ta ] refreshed trust anchors for domain ' .. kres.dname2str(keyset.owner) .. ' are:\n' - .. trust_anchors.summary(keyset.owner)) - end - - return true -end - -local add_file = function (path, unmanaged) - if not unmanaged then if not io.open(path .. '.lock', 'w') then error("[ ta ] ERROR: write access needed to keyfile dir '"..path.."'") end os.remove(path .. ".lock") end - -- Bootstrap if requested and keyfile doesn't exist - if not unmanaged and not io.open(path, 'r') then + -- Bootstrap TA for root zone if keyfile doesn't exist + if managed and not io.open(path, 'r') then + if trust_anchors.keysets['\0'] then + error(string.format( + "[ ta ] keyfile '%s' doesn't exist and root key is already installed, " + .. "cannot bootstrap; provide a path to valid file with keys", path)) + end log("[ ta ] keyfile '%s': doesn't exist, bootstrapping", path); - local tas, msg = bootstrap(trust_anchors.bootstrap_url, trust_anchors.bootstrap_ca) - if not tas then + local rrstr, msg = bootstrap(trust_anchors.bootstrap_url, trust_anchors.bootstrap_ca) + if not rrstr then msg = msg .. '\n' .. '[ ta ] Failed to bootstrap root trust anchors!' error(msg) end print(msg) - trust_anchors.add(tas) - -- Fetch DNSKEY immediately - local keyset = trust_anchors.keysets['\0'] - keyset.filename = path - if keyset.refresh_ev then event.cancel(keyset.refresh_ev) end - refresh_plan(keyset, 0, true) - return - end - if not unmanaged and path == (trust_anchors.keysets['\0'] or {}).filename then - return + bootstrap_write(rrstr, path) + -- continue as if the keyfile was there end -- Parse the file and check its sanity @@ -557,25 +357,45 @@ local add_file = function (path, unmanaged) if not keyset then panic("[ ta ] ERROR: failed to read anchors from '%s' (%s)", path, err) end - if not unmanaged then keyset.filename = path end + keyset.filename = path + keyset.managed = managed local owner = keyset.owner local owner_str = kres.dname2str(owner) - if trust_anchors.keysets[owner] then + local keyset_orig = trust_anchors.keysets[owner] + if keyset_orig then warn('[ ta ] warning: overriding previously set trust anchors for ' .. owner_str) - local refresh_ev = trust_anchors.keysets[owner].refresh_ev - if refresh_ev then event.cancel(refresh_ev) end + if keyset_orig.managed and ta_update then + ta_update.stop(owner) + end end trust_anchors.keysets[owner] = keyset -- Replace the TA store used for validation if keyset_publish(keyset) and verbose() then log('[ ta ] installed trust anchors for domain ' .. owner_str .. ' are:\n' - .. trust_anchors.summary(owner)) + .. trust_anchors.summary(owner)) end -- TODO: if failed and for root, try to rebootstrap? - if not unmanaged then refresh_plan(keyset, 10 * sec, false) end + if managed then + ta_update.start(owner) + end +end + +local function remove(zname) + local owner = kres.str2dname(zname) + if not trust_anchors.keysets[owner] then + return false + end + + if ta_update then + ta_update.stop(owner) + end + trust_anchors.keysets[owner] = nil + local store = kres.context().trust_anchors + C.kr_ta_del(store, owner) + return true end local function ta_str(owner) @@ -609,25 +429,24 @@ trust_anchors = { -- - owner - that dname (for simplicity) -- - [optional] filename in which to persist the state, -- implying unmanaged TA if nil - -- - [optional] overrides for global defaults of - -- hold_down_time, refresh_time, keep_removed -- The RR tables also contain some additional TA-specific fields. keysets = {}, -- Documented properties: insecure = {}, - hold_down_time = 30 * day, - refresh_time = nil, - keep_removed = 0, bootstrap_url = 'https://data.iana.org/root-anchors/root-anchors.xml', bootstrap_ca = '@etc_dir@/icann-ca.pem', - keyfile_default = '@keyfile_default@', -- Load keys from a file, 5011-managed by default. -- If managed and the file doesn't exist, try bootstrapping the root into it. add_file = add_file, - config = add_file, + config = function() upgrade_required('trust_anchors.config was removed, use trust_anchors.add_file()') end, + remove = remove, + + keyset_publish = keyset_publish, + keyset_write = keyset_write, + key_state = key_state, -- Add DS/DNSKEY record(s) (unmanaged) add = function (keystr) @@ -638,15 +457,15 @@ trust_anchors = { local keyset_orig = trust_anchors.keysets[owner] -- Set up trust_anchors.keysets[owner] if keyset_orig then + if keyset_orig.managed then + panic('[ ta ] it is impossible to add an unmanaged TA for zone ' + .. owner_str .. ' which already has a managed TA') + end warn('[ ta ] warning: extending previously set trust anchors for ' .. owner_str) for _, ta in ipairs(keyset) do table.insert(keyset_orig, ta) end - -- we might also add more warning if it's managed, i.e. has .filename, - -- as the next update would overwrite this additional TA - else - trust_anchors.keysets[owner] = keyset end -- Replace the TA store used for validation if not keyset_publish(keyset) then @@ -654,6 +473,9 @@ trust_anchors = { -- trust_anchors.keysets[owner] was already updated to the -- (partially) failing state, but I'm not sure how much to improve this end + keyset.managed = false + trust_anchors.keysets[owner] = keyset + end if verbose() or err then log('New TA state:\n' .. trust_anchors.summary()) end if err then @@ -703,11 +525,15 @@ trust_anchors = { -- Syntactic sugar for TA store setmetatable(trust_anchors, { - __newindex = function (t,k,v) - if k == 'file' then t.config(v) - elseif k == 'negative' then t.set_insecure(v) - else rawset(t, k, v) end - end, + __newindex = function (t,k,v) + if k == 'file' then + upgrade_required('trust_anchors.file was removed, use trust_anchors.add_file()') + elseif k == 'negative' then + upgrade_required('trust_anchors.negative was removed, use trust_anchors.set_insecure()') + elseif k == 'keyfile_default' then + upgrade_required('trust_anchors.keyfile_default is now compiled in, see trust_anchors.remove()') + else rawset(t, k, v) end + end, }) return trust_anchors diff --git a/daemon/lua/trust_anchors.test/bootstrap.test.lua b/daemon/lua/trust_anchors.test/bootstrap.test.lua index ea35c487eb11972872040a0943fe95c02edf90cd..8ea20a8ab4273fffba45fc90162a63ab78d1092d 100644 --- a/daemon/lua/trust_anchors.test/bootstrap.test.lua +++ b/daemon/lua/trust_anchors.test/bootstrap.test.lua @@ -60,7 +60,7 @@ end local host = 'https://localhost:8080/' -- avoid interference with configured keyfile_default -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') local function test_err_cert() trust_anchors.bootstrap_ca = 'x509/wrongca.pem' @@ -84,6 +84,7 @@ end local function test_ok_xml(testname, testdesc) return function() trust_anchors.bootstrap_url = host .. testname .. '.xml' + trust_anchors.remove('.') same(trust_anchors.add_file(testname .. '.keys'), nil, testdesc) end end diff --git a/daemon/lua/trust_anchors.test/root.keys b/daemon/lua/trust_anchors.test/root.keys new file mode 100644 index 0000000000000000000000000000000000000000..e292b5a7bf0cc4afbefdee17c56b10edbd2126f3 --- /dev/null +++ b/daemon/lua/trust_anchors.test/root.keys @@ -0,0 +1 @@ +. IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D diff --git a/daemon/lua/trust_anchors.test/ta.test.lua b/daemon/lua/trust_anchors.test/ta.test.lua index 97da60da53cacaa048b7853a0e1199926b0d843a..2071152d50ba7cf4cf6a0f5f855d55d2eb017e70 100644 --- a/daemon/lua/trust_anchors.test/ta.test.lua +++ b/daemon/lua/trust_anchors.test/ta.test.lua @@ -1,6 +1,18 @@ +trust_anchors.remove('.') local ffi = require('ffi') +-- count warning messages +warn_msg = {} +overriding_msg="[ ta ] warning: overriding previously set trust anchors for ." +warn_msg[overriding_msg] = 0 +function warn(fmt, ...) + msg = string.format(fmt, ...) + if warn_msg[msg] ~= nil then + warn_msg[msg] = warn_msg[msg] + 1 + end +end + -- Test that adding a revoked DNSKEY is refused. local function test_revoked_key() local ta_c = kres.context().trust_anchors @@ -21,8 +33,40 @@ local function test_revoked_key() same(root_ta.rrs.count, 1, 'the root TA set contains one RR') end +local function test_remove() + -- uses root key from the previous test + assert(trust_anchors.keysets['\0'], 'root key must be there from previous test') + local ta_c = kres.context().trust_anchors + local root_ta = ffi.C.kr_ta_get(ta_c, '\0') + assert(root_ta ~= nil, 'we got non-NULL TA RRset') + assert(root_ta.rrs.count, 1, 'we have a root TA set to be deleted') + + trust_anchors.remove('.') + + same(trust_anchors.keysets['\0'], nil, 'Lua interface does not have the removed key') + root_ta = ffi.C.kr_ta_get(ta_c, '\0') + same(root_ta == nil, true, 'C interface does not have the removed key') +end + +local function test_add_file() + boom(trust_anchors.add_file, {'nonwriteable/root.keys', false}, + "Managed trust anchor in non-writeable directory") + + boom(trust_anchors.add_file, {'nonexist.keys', true}, + "Nonexist unmanaged trust anchor file") + + is(warn_msg[overriding_msg], 0, "No override warning messages at start of test") + trust_anchors.add_file('root.keys', true) + trust_anchors.add_file('root.keys', true) + is(warn_msg[overriding_msg], 1, "Warning message when override trust anchors") + + is(trust_anchors.keysets['\0'][1].key_tag, 20326, + "Loaded KeyTag from root.keys") +end return { - test_revoked_key() + test_revoked_key, + test_remove, + test_add_file, } diff --git a/daemon/main.c b/daemon/main.c index 4f94c1e180c7873abe2fa39cb9bd4deafdd11e81..aee9d378ffe62bc65e0fcaef5b95942482aa755c 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -58,8 +58,6 @@ struct args { addr_array_t tls_set; fd_array_t fd_set; fd_array_t tls_fd_set; - char *keyfile; - int keyfile_unmanaged; const char *config; int control_fd; const char *rundir; @@ -68,16 +66,6 @@ struct args { bool tty_binary_output; }; -/* lua_pcall helper function */ -static inline char *lua_strerror(int lua_err) { - switch (lua_err) { - case LUA_ERRRUN: return "a runtime error"; - case LUA_ERRMEM: return "memory allocation error."; - case LUA_ERRERR: return "error while running the error handler function."; - default: return "a unknown error"; - } -} - /** * TTY control: process input and free() the buffer. * @@ -384,8 +372,6 @@ static void help(int argc, char *argv[]) " -S, --fd=[fd] Listen on given fd (handed out by supervisor).\n" " -T, --tlsfd=[fd] Listen using TLS on given fd (handed out by supervisor).\n" " -c, --config=[path] Config file path (relative to [rundir]) (default: config).\n" - " -k, --keyfile=[path] File with root domain trust anchors (DS or DNSKEY), automatically updated.\n" - " -K, --keyfile-ro=[path] File with read-only root domain trust anchors, for use with an external updater.\n" " -f, --forks=N Start N forks sharing the configuration.\n" " -q, --quiet No command prompt in interactive mode.\n" " -v, --verbose Run in verbose mode." @@ -483,31 +469,6 @@ static void free_sd_socket_names(char **socket_names, int count) } #endif -static int set_keyfile(struct engine *engine, char *keyfile, bool unmanaged) -{ - assert(keyfile != NULL); - auto_free char *cmd = afmt("trust_anchors.config('%s',%s)", - keyfile, unmanaged ? "true" : "nil"); - if (!cmd) { - kr_log_error("[system] not enough memory\n"); - return kr_error(ENOMEM); - } - int lua_ret = engine_cmd(engine->L, cmd, false); - if (lua_ret != 0) { - if (lua_gettop(engine->L) > 0) { - kr_log_error("%s\n", lua_tostring(engine->L, -1)); - } else { - kr_log_error("[ ta ] keyfile '%s': failed to load (%s)\n", - keyfile, lua_strerror(lua_ret)); - } - return lua_ret; - } - - lua_settop(engine->L, 0); - return kr_ok(); -} - - static void args_init(struct args *args) { memset(args, 0, sizeof(struct args)); @@ -542,8 +503,6 @@ static int parse_args(int argc, char **argv, struct args *args) {"fd", required_argument, 0, 'S'}, {"tlsfd", required_argument, 0, 'T'}, {"config", required_argument, 0, 'c'}, - {"keyfile", required_argument, 0, 'k'}, - {"keyfile-ro", required_argument, 0, 'K'}, {"forks", required_argument, 0, 'f'}, {"verbose", no_argument, 0, 'v'}, {"quiet", no_argument, 0, 'q'}, @@ -578,15 +537,6 @@ static int parse_args(int argc, char **argv, struct args *args) return EXIT_FAILURE; } break; - case 'K': - args->keyfile_unmanaged = 1; - case 'k': - if (args->keyfile != NULL) { - kr_log_error("[system] error only one of '--keyfile' and '--keyfile-ro' allowed\n"); - return EXIT_FAILURE; - } - args->keyfile = optarg; - break; case 'v': kr_verbose_set(true); #ifdef NOVERBOSELOG @@ -800,10 +750,6 @@ int main(int argc, char **argv) } lua_settop(engine.L, 0); } - if (args.keyfile != NULL && set_keyfile(&engine, args.keyfile, args.keyfile_unmanaged) != 0) { - ret = EXIT_FAILURE; - goto cleanup; - } if (args.config == NULL || strcmp(args.config, "-") !=0) { if(engine_load_defaults(&engine) != 0) { ret = EXIT_FAILURE; diff --git a/doc/kresd.8.in b/doc/kresd.8.in index 524c7eacb8cce449b8bac958eb860529fdb92b5c..8bd87fc9e4fb9898c2760d92d35007733e841ad8 100644 --- a/doc/kresd.8.in +++ b/doc/kresd.8.in @@ -22,10 +22,6 @@ .IR fd ] .RB [ \-c | \-\-config .IR config ] -.RB [ \-k | \-\-keyfile -.IR keyfile ] -.RB [ \-K | \-\-keyfile\-ro -.IR keyfile ] .RB [ \-f | \-\-forks .IR N ] .RB [ \-q | \-\-quiet ] @@ -69,7 +65,7 @@ and start .PP .nf .RS 6n -$ kresd -a 127.0.0.1 -k root.keys +$ kresd -a 127.0.0.1 [system] interactive mode > .RE @@ -90,7 +86,7 @@ online documentation. $ cat << EOF > config modules = { 'policy' } policy.add(policy.all(policy.FORWARD('192.168.1.1'))) -$ kresd -a 127.0.0.1 -k root.keys +$ kresd -a 127.0.0.1 EOF .RE .fi @@ -120,22 +116,6 @@ Set the config file with settings for kresd to read instead of reading the file at the default location (\fIconfig\fR). The syntax is described in \fIdaemon/README.md\fR. .TP -.B \-k\fI keyfile\fR, \fB\-\-keyfile=\fI<keyfile> -(Recommended!) Automatically managed root trust anchors file. -Root trust anchors in this file are managed using standard RFC 5011 (Automated Updates of DNS Security Trust Anchors). -Kresd needs write access to the directory containing the keyfile. - -If the file does not exist, it will be automatically boostrapped from IANA using HTTPS protocol -and warning that you need to to check the key before trusting it will be issued. - -The file contains DNSKEY/DS records in presentation format, -and is compatible with Unbound and BIND 9 root key files. -@man_managed_keyfile_default@ -.TP -.B \-K\fI keyfile\fR, \fB\-\-keyfile\-ro=\fI<keyfile> -(Discouraged) Static root trust anchors file. The file is not updated by kresd. Use of this option is discouraged because it will break your installation when the trust anchor key changes! -@man_unmanaged_keyfile_default@ -.TP .B \-f\fI N\fR, \fB\-\-forks=\fI<N> With this option, the daemon is started in non-interactive mode and instead creates a UNIX socket in \fIrundir\fR that the operator can connect to for interactive session. diff --git a/doc/meson.build b/doc/meson.build index 8c912829e558d70a79e16a28712a5f790506ea7c..1860427895e84af8085759393c6420e4a7bc3018 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -4,7 +4,6 @@ man_config = configuration_data() man_config.set('version', meson.project_version()) man_config.set('date', run_command('../scripts/get-date.sh').stdout()) -man_config.set('keyfile_default', keyfile_default) man_config.set('man_seealso_systemd', '') if systemd_files == 'enabled' @@ -13,16 +12,6 @@ elif systemd_files == 'nosocket' man_config.set('man_seealso_systemd', '\\fIkresd.systemd.nosocket(7)\\fR, ') endif -man_config.set('man_managed_keyfile_default', '') -man_config.set('man_unmanaged_keyfile_default', '') -if managed_ta - man_config.set('man_managed_keyfile_default', ''' -Default: "@0@"'''.format(keyfile_default)) -else - man_config.set('man_unmanaged_keyfile_default', ''' -Default: "@0@"'''.format(keyfile_default)) -endif - man_kresd = configure_file( input: 'kresd.8.in', output: 'kresd.8', diff --git a/doc/upgrading.rst b/doc/upgrading.rst index 1ea5e19904dfbf161d141a804a05c42c4d367b33..013d41c0f696f4abb520c1eab1679fce30fc99df 100644 --- a/doc/upgrading.rst +++ b/doc/upgrading.rst @@ -15,10 +15,34 @@ Users * DNSSEC validation is now turned on by default. If you need to disable it, see :ref:`dnssec-config`. +* ``-k/--keyfile`` and ``-K/--keyfile-ro`` daemon options were removed. If needed, + use ``trust_anchors.add_file()`` in configuration file instead. * In case you are using your own custom modules, move them to the new module location. The exact location depends on your distribution. Generally, modules previously in ``/usr/lib/kdns_modules`` should be moved to ``/usr/lib/knot-resolver/kres_modules``. +Configuration +~~~~~~~~~~~~~ + +* ``trust_anchors.file``, ``trust_anchors.config()`` and ``trust_anchors.negative`` + aliases were removed to avoid duplicity + + .. csv-table:: + :header: "3.x configuration", "4.x configuration" + + "``trust_anchors.file = path``", "``trust_anchors.add_file(path)``" + "``trust_anchors.config(path, readonly)``", "``trust_anchors.add_file(path, readonly)``" + "``trust_anchors.negative = nta_set``", "``trust_anchors.set_insecure(nta_set)``" + +* ``trust_anchors.keyfile_default`` is no longer accessible and is only possible to set + at compile time. To turn off DNSSEC, use ``trust_anchors.remove('.')``. + + .. csv-table:: + :header: "3.x configuration", "4.x configuration" + + "``trust_anchors.keyfile_default = nil``", "``trust_anchors.remove('.')``" + + Packagers & Developers ---------------------- diff --git a/etc/config/config.cluster.in b/etc/config/config.cluster.in index 709116b04bd3731eacd0b5757e3f431a6035a3df..6984aa0920e80870bdb67b0a45ba6296718283d0 100644 --- a/etc/config/config.cluster.in +++ b/etc/config/config.cluster.in @@ -7,7 +7,7 @@ @config_defaults@ -- To disable DNSSEC validation, uncomment the following line (not recommended) --- trust_anchors.keyfile_default = nil +-- trust_anchors.remove('.') -- Large cache size, so we don't need to flush ever -- This can be larger than available RAM, least frequently accessed diff --git a/etc/config/config.docker.in b/etc/config/config.docker.in index 4e284af760a334b2cb73da9d3de85a56badcfcfb..1a54f1638adbf28ffb2fdcfbae0afd0def9542d0 100644 --- a/etc/config/config.docker.in +++ b/etc/config/config.docker.in @@ -6,7 +6,7 @@ net.listen('0.0.0.0') net.listen('0.0.0.0', 853, {tls=true}) -- To disable DNSSEC validation, uncomment the following line (not recommended) --- trust_anchors.keyfile_default = nil +-- trust_anchors.remove('.') -- Load Useful modules modules = { diff --git a/etc/config/config.isp.in b/etc/config/config.isp.in index 30ddbbe00ec81bac2c3a4f357f4162c300a5e0da..4c29db19aa312d373d61dd5e362fcd7b52978098 100644 --- a/etc/config/config.isp.in +++ b/etc/config/config.isp.in @@ -4,7 +4,7 @@ @config_defaults@ -- To disable DNSSEC validation, uncomment the following line (not recommended) --- trust_anchors.keyfile_default = nil +-- trust_anchors.remove('.') -- Large cache size, so we don't need to flush often -- This can be larger than available RAM, least frequently accessed diff --git a/etc/config/config.personal.in b/etc/config/config.personal.in index 555edc3590aea005f87458571ea2ef499c4d84b0..6d9844e020fec97b41fcf7f6567b1093be8bf2be 100644 --- a/etc/config/config.personal.in +++ b/etc/config/config.personal.in @@ -3,7 +3,7 @@ @config_defaults@ -- To disable DNSSEC validation, uncomment the following line (not recommended) --- trust_anchors.keyfile_default = nil +-- trust_anchors.remove('.') -- Load useful modules modules = { diff --git a/etc/config/config.splitview.in b/etc/config/config.splitview.in index ae009ac77402f761201a8d4d9fdf01419ef2e048..60da665d79c4b8ce7684f9e91b14e8f19d65c3cc 100644 --- a/etc/config/config.splitview.in +++ b/etc/config/config.splitview.in @@ -4,7 +4,7 @@ @config_defaults@ -- To disable DNSSEC validation, uncomment the following line (not recommended) --- trust_anchors.keyfile_default = nil +-- trust_anchors.remove('.') -- Load Useful modules modules = { diff --git a/meson.build b/meson.build index 8cb43d0c85eaffc650b7eb7a36405c257bb5e3fe..73fb06cba87a5addfdaa84e8b8e158521ef718f3 100644 --- a/meson.build +++ b/meson.build @@ -108,7 +108,7 @@ unit_tests = [ config_tests = [ # [name, files(test)] # or - # [name, files(test), [arg1, arg2], should_fail] + # [name, files(test), [extra_suites]] ] integr_tests = [ diff --git a/modules/bogus_log/test.integr/kresd_config.j2 b/modules/bogus_log/test.integr/kresd_config.j2 index bf4b1e85f4d542238e212d7c64a11c2068452917..061c841709ab223c7811e14d95ad21ac775ae89f 100644 --- a/modules/bogus_log/test.integr/kresd_config.j2 +++ b/modules/bogus_log/test.integr/kresd_config.j2 @@ -34,6 +34,11 @@ function reply_result(state, req) end policy.add(policy.pattern(reply_result, 'bogus_log.test.')) +-- Disable RFC5011 TA update +if ta_update then + modules.unload('ta_update') +end + -- Disable RFC8145 signaling, scenario doesn't provide expected answers if ta_signal_query then modules.unload('ta_signal_query') diff --git a/modules/meson.build b/modules/meson.build index 0c3d7fb0dfad059c7653b097dd30cec593bbf16b..5df7fedd8ca0419ae5936139ea822b084baeffac 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -14,6 +14,7 @@ lua_mod_src = [ # add lua modules without separate meson.build files('serve_stale/serve_stale.lua'), files('ta_sentinel/ta_sentinel.lua'), files('ta_signal_query/ta_signal_query.lua'), + files('ta_update/ta_update.lua'), files('workarounds/workarounds.lua'), ] @@ -22,12 +23,16 @@ config_tests += [ ['hints', files('hints/tests/hints.test.lua')], ['nsid', files('nsid/nsid.test.lua')], ['dns64', files('dns64/dns64.test.lua')], + ['ta_update', files('ta_update/ta_update.test.lua')], ] integr_tests += [ ['bogus_log', join_paths(meson.current_source_dir(), 'bogus_log', 'test.integr')], ['rebinding', join_paths(meson.current_source_dir(), 'rebinding', 'test.integr')], ['serve_stale', join_paths(meson.current_source_dir(), 'serve_stale', 'test.integr')], + # NOTE: ta_update may pass in cases when it should fail due to race conditions + # To ensure reliability, deckard should introduce a time wait + ['ta_update', join_paths(meson.current_source_dir(), 'ta_update', 'ta_update.test.integr')], ] diff --git a/modules/policy/noipv6.test.integr/kresd_config.j2 b/modules/policy/noipv6.test.integr/kresd_config.j2 index 5648d81eed750332f8b5a2190545f9e92d30a8f9..93099f37e2bf94a359640b433d25219f1b8dab9e 100644 --- a/modules/policy/noipv6.test.integr/kresd_config.j2 +++ b/modules/policy/noipv6.test.integr/kresd_config.j2 @@ -3,7 +3,12 @@ net.ipv6 = false policy.add(policy.all(policy.STUB({ '::1:2:3:4', '1.2.3.4' }))) -- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') + +-- Disable RFC5011 TA update +if ta_update then + modules.unload('ta_update') +end -- Disable RFC8145 signaling, scenario doesn't provide expected answers if ta_signal_query then diff --git a/modules/policy/noipvx.test.integr/kresd_config.j2 b/modules/policy/noipvx.test.integr/kresd_config.j2 index 9ee7afd2fb53b6bc7fee31c11a95574fcccf77cc..ce97967970a42fe29449b7b634790b9800eb64fa 100644 --- a/modules/policy/noipvx.test.integr/kresd_config.j2 +++ b/modules/policy/noipvx.test.integr/kresd_config.j2 @@ -4,7 +4,12 @@ net.ipv6 = false policy.add(policy.all(policy.STUB({ '::1:2:3:4', '1.2.3.4' }))) -- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') + +-- Disable RFC5011 TA update +if ta_update then + modules.unload('ta_update') +end -- Disable RFC8145 signaling, scenario doesn't provide expected answers if ta_signal_query then diff --git a/modules/policy/test.integr/kresd_config.j2 b/modules/policy/test.integr/kresd_config.j2 index 5ad54fea4c11df895cfc4f9e618cda3b10c86f42..3225c537918968fdd1b110900ebaa298b2314d63 100644 --- a/modules/policy/test.integr/kresd_config.j2 +++ b/modules/policy/test.integr/kresd_config.j2 @@ -2,7 +2,12 @@ policy.add(policy.suffix(policy.REFUSE, {todname('refuse.example.com')})) -- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') + +-- Disable RFC5011 TA update +if ta_update then + modules.unload('ta_update') +end -- Disable RFC8145 signaling, scenario doesn't provide expected answers if ta_signal_query then diff --git a/modules/rebinding/test.integr/kresd_config.j2 b/modules/rebinding/test.integr/kresd_config.j2 index 39bc957bde556f2fa24bdd7eb92aa3f1a9a3f7e9..e044351f9872a21be13c71d0ee9ad41b46ffa2a1 100644 --- a/modules/rebinding/test.integr/kresd_config.j2 +++ b/modules/rebinding/test.integr/kresd_config.j2 @@ -1,6 +1,11 @@ {% raw %} -- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') + +-- Disable RFC5011 TA update +if ta_update then + modules.unload('ta_update') +end -- Disable RFC8145 signaling, scenario doesn't provide expected answers if ta_signal_query then diff --git a/modules/serve_stale/test.integr/kresd_config.j2 b/modules/serve_stale/test.integr/kresd_config.j2 index b9a242c22ff73729ad333ba8f777a0657a63417d..5beff9e3f7c621ed0dc985f08fe825d56ad5ef0e 100644 --- a/modules/serve_stale/test.integr/kresd_config.j2 +++ b/modules/serve_stale/test.integr/kresd_config.j2 @@ -2,7 +2,12 @@ modules = { 'serve_stale < cache' } -- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') + +-- Disable RFC5011 TA update +if ta_update then + modules.unload('ta_update') +end -- Disable RFC8145 signaling, scenario doesn't provide expected answers if ta_signal_query then diff --git a/modules/stats/test.integr/kresd_config.j2 b/modules/stats/test.integr/kresd_config.j2 index b8fd49816581a077ba673a06426a42787620231b..a93ece40b3c139e66add0ec1c230a9dbda982c51 100644 --- a/modules/stats/test.integr/kresd_config.j2 +++ b/modules/stats/test.integr/kresd_config.j2 @@ -52,7 +52,12 @@ policy.add(policy.pattern(reply_result, 'stats.test.')) policy.add(policy.all(FWD_TARGET)) -- avoid iteration -- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') + +-- Disable RFC5011 TA update +if ta_update then + modules.unload('ta_update') +end -- Disable RFC8145 signaling, scenario doesn't provide expected answers if ta_signal_query then diff --git a/modules/ta_update/root.keys b/modules/ta_update/root.keys new file mode 100644 index 0000000000000000000000000000000000000000..e292b5a7bf0cc4afbefdee17c56b10edbd2126f3 --- /dev/null +++ b/modules/ta_update/root.keys @@ -0,0 +1 @@ +. IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D diff --git a/modules/ta_update/ta_update.lua b/modules/ta_update/ta_update.lua new file mode 100644 index 0000000000000000000000000000000000000000..4c0b47624fb188e4991407b423512f0d4cb30509 --- /dev/null +++ b/modules/ta_update/ta_update.lua @@ -0,0 +1,287 @@ +-- Module interface +local ffi = require('ffi') +local kres = require('kres') +local C = ffi.C + +assert(trust_anchors, 'ta_update module depends on initialized trust_anchors library') +local key_state = trust_anchors.key_state +assert(key_state) + +local ta_update = {} +local tracked_tas = {} -- zone name (wire) => {event = number} + + +-- Find key in current keyset +local function ta_find(keyset, rr) + local rr_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata) + if rr_tag < 0 or rr_tag > 65535 then + warn(string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s', + kres.rr2str(rr), ffi.string(C.knot_strerror(rr_tag)))) + return nil + end + for i, ta in ipairs(keyset) do + -- Match key owner and content + local ta_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata) + if ta_tag < 0 or ta_tag > 65535 then + warn(string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s', + kres.rr2str(ta), ffi.string(C.knot_strerror(ta_tag)))) + else + if ta.owner == rr.owner then + if ta.type == rr.type then + if rr.type == kres.type.DNSKEY then + if C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) == 0 then + return ta + end + elseif rr.type == kres.type.DS and ta.rdata == rr.rdata then + return ta + end + -- DNSKEY superseding DS, inexact match + elseif rr.type == kres.type.DNSKEY and ta.type == kres.type.DS then + if ta.key_tag == rr_tag then + keyset[i] = rr -- Replace current DS + rr.state = ta.state + rr.key_tag = ta.key_tag + return rr + end + -- DS key matching DNSKEY, inexact match + elseif rr.type == kres.type.DS and ta.type == kres.type.DNSKEY then + if rr_tag == ta_tag then + return ta + end + end + end + end + end + return nil +end + +-- Evaluate TA status of a RR according to RFC5011. The time is in seconds. +local function ta_present(keyset, rr, hold_down_time) +if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then + return false -- Ignore +end +-- Attempt to extract key_tag +local key_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata) +if key_tag < 0 or key_tag > 65535 then + warn(string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s', + kres.rr2str(rr), ffi.string(C.knot_strerror(key_tag)))) + return false +end +-- Find the key in current key set and check its status +local now = os.time() +local key_revoked = (rr.type == kres.type.DNSKEY) and C.kr_dnssec_key_revoked(rr.rdata) +local ta = ta_find(keyset, rr) +if ta then + -- Key reappears (KeyPres) + if ta.state == key_state.Missing then + ta.state = key_state.Valid + ta.timer = nil + end + -- Key is revoked (RevBit) + if ta.state == key_state.Valid or ta.state == key_state.Missing then + if key_revoked then + ta.state = key_state.Revoked + ta.timer = now + hold_down_time + end + end + -- Remove hold-down timer expires (RemTime) + if ta.state == key_state.Revoked and os.difftime(ta.timer, now) <= 0 then + ta.state = key_state.Removed + ta.timer = nil + end + -- Add hold-down timer expires (AddTime) + if ta.state == key_state.AddPend and os.difftime(ta.timer, now) <= 0 then + ta.state = key_state.Valid + ta.timer = nil + end + if rr.state ~= key_state.Valid or verbose() then + log('[ta_update] key: ' .. key_tag .. ' state: '..ta.state) + end + return true +elseif not key_revoked then -- First time seen (NewKey) + rr.key_tag = key_tag + return false +end +end + +-- TA is missing in the new key set. The time is in seconds. +local function ta_missing(ta, hold_down_time) + -- Key is removed (KeyRem) + local keep_ta = true + local key_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata) + if key_tag < 0 or key_tag > 65535 then + warn(string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s', + kres.rr2str(ta), ffi.string(C.knot_strerror(key_tag)))) + key_tag = '' + end + if ta.state == key_state.Valid then + ta.state = key_state.Missing + ta.timer = os.time() + hold_down_time + + -- Remove key that is missing for too long + elseif ta.state == key_state.Missing and os.difftime(ta.timer, os.time()) <= 0 then + ta.state = key_state.Removed + log('[ta_update] key: '..key_tag..' removed because missing for too long') + keep_ta = false + + -- Purge pending key + elseif ta.state == key_state.AddPend then + log('[ta_update] key: '..key_tag..' purging') + keep_ta = false + end + log('[ta_update] key: '..key_tag..' state: '..ta.state) + return keep_ta +end + +-- Update existing keyset; return true if successful. +local function update(keyset, new_keys) + if not new_keys then return false end + if not keyset.managed then + -- this may happen due to race condition during testing in CI (refesh time < query time) + return false + end + + -- Filter TAs to be purged from the keyset (KeyRem), in three steps + -- 1: copy TAs to be kept to `keepset` + local hold_down = (keyset.hold_down_time or ta_update.hold_down_time) / 1000 + local keepset = {} + local keep_removed = keyset.keep_removed or ta_update.keep_removed + for _, ta in ipairs(keyset) do + local keep = true + if not ta_find(new_keys, ta) then + -- Ad-hoc: RFC 5011 doesn't mention removing a Missing key. + -- Let's do it after a very long period has elapsed. + keep = ta_missing(ta, hold_down * 4) + end + -- Purge removed keys + if ta.state == key_state.Removed then + if keep_removed > 0 then + keep_removed = keep_removed - 1 + else + keep = false + end + end + if keep then + table.insert(keepset, ta) + end + end + -- 2: remove all TAs - other settings etc. will remain in the keyset + for i, _ in ipairs(keyset) do + keyset[i] = nil + end + -- 3: move TAs to be kept into the keyset (same indices) + for k, ta in pairs(keepset) do + keyset[k] = ta + end + + -- Evaluate new TAs + for _, rr in ipairs(new_keys) do + if (rr.type == kres.type.DNSKEY or rr.type == kres.type.DS) and rr.rdata ~= nil then + ta_present(keyset, rr, hold_down) + end + end + + -- Store the keyset + trust_anchors.keyset_write(keyset) + + -- Start using the new TAs. + if not trust_anchors.keyset_publish(keyset) then + -- TODO: try to rebootstrap if for root? + return false + elseif verbose() then + log('[ta_update] refreshed trust anchors for domain ' .. kres.dname2str(keyset.owner) .. ' are:\n' + .. trust_anchors.summary(keyset.owner)) + end + + return true +end + +-- Refresh the DNSKEYs from the packet, and return time to the next check. +local function active_refresh(keyset, pkt) + local retry = true + if pkt:rcode() == kres.rcode.NOERROR then + local records = pkt:section(kres.section.ANSWER) + local new_keys = {} + for _, rr in ipairs(records) do + if rr.type == kres.type.DNSKEY then + table.insert(new_keys, rr) + end + end + update(keyset, new_keys) + retry = false + else + warn('[ta_update] active refresh failed for ' .. kres.dname2str(keyset.owner) + .. ' with rcode: ' .. pkt:rcode()) + end + -- Calculate refresh/retry timer (RFC 5011, 2.3) + local min_ttl = retry and day or 15 * day + for _, rr in ipairs(keyset) do -- 10 or 50% of the original TTL + min_ttl = math.min(min_ttl, (retry and 100 or 500) * rr.ttl) + end + return math.max(hour, min_ttl) +end + +-- Plan an event for refreshing DNSKEYs and re-scheduling itself +local function refresh_plan(keyset, delay) + local owner = keyset.owner + local owner_str = kres.dname2str(keyset.owner) + if not tracked_tas[owner] then + tracked_tas[owner] = {} + end + local track_cfg = tracked_tas[owner] + if track_cfg.event then -- restart timer if necessary + event.cancel(track_cfg.event) + end + track_cfg.event = event.after(delay, function () + log('[ta_update] refreshing TA for ' .. owner_str) + resolve(owner_str, kres.type.DNSKEY, kres.class.IN, 'NO_CACHE', + function (pkt) + -- Schedule itself with updated timeout + local delay_new = active_refresh(keyset, kres.pkt_t(pkt)) + delay_new = keyset.refresh_time or ta_update.refresh_time or delay_new + log('[ta_update] next refresh for ' .. owner_str .. ' in ' + .. delay_new/hour .. ' hours') + refresh_plan(keyset, delay_new) + end) + end) +end + +ta_update = { + -- [optional] overrides for global defaults of + -- hold_down_time, refresh_time, keep_removed + hold_down_time = 30 * day, + refresh_time = nil, + keep_removed = 0, + tracked = tracked_tas, -- debug and visibility, should not be changed by hand +} + +-- start tracking (already loaded) TA with given zone name in wire format +-- do first refresh immediatelly +function ta_update.start(zname) + local keyset = trust_anchors.keysets[zname] + if not keyset then + panic('[ta_update] TA must be configured first before tracking it') + end + if not keyset.managed then + panic('[ta_update] TA is configured as unmanaged; remove it and ' + .. 'add it again as managed using trust_anchors.add_file()') + end + refresh_plan(keyset, 0) +end + +function ta_update.stop(zname) + if tracked_tas[zname] then + event.cancel(tracked_tas[zname].event) + tracked_tas[zname] = nil + trust_anchors.keysets[zname].managed = false + end +end + +-- stop all timers +function ta_update.deinit() + for zname, _ in pairs(tracked_tas) do + ta_update.stop(zname) + end +end + +return ta_update diff --git a/modules/ta_update/ta_update.test.integr/deckard.yaml b/modules/ta_update/ta_update.test.integr/deckard.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6906eeb1b977f9b50faaac68244766d135035e27 --- /dev/null +++ b/modules/ta_update/ta_update.test.integr/deckard.yaml @@ -0,0 +1,12 @@ +programs: +- name: kresd + binary: kresd + additional: + - -f + - "1" + templates: + - modules/ta_update/ta_update.test.integr/kresd_config.j2 + - tests/integration/hints_zone.j2 + configs: + - config + - hints diff --git a/tests/integration/kresd_config.j2 b/modules/ta_update/ta_update.test.integr/kresd_config.j2 similarity index 63% rename from tests/integration/kresd_config.j2 rename to modules/ta_update/ta_update.test.integr/kresd_config.j2 index d7f4cfb663887b674127b0c41b282257447d5359..e1b03a5c8f66308eeecc75e50364e0fc947cfa87 100644 --- a/tests/integration/kresd_config.j2 +++ b/modules/ta_update/ta_update.test.integr/kresd_config.j2 @@ -1,11 +1,30 @@ -net = { '{{SELF_ADDR}}' } --- hints.root({['k.root-servers.net'] = '{{ROOT_ADDR}}'}) -_hint_root_file('hints') +{% for TAF in TRUST_ANCHOR_FILES %} +trust_anchors.add_file('{{TAF}}') +{% endfor %} + +{% raw %} + +-- Disable RFC8145 signaling, scenario doesn't provide expected answers +if ta_signal_query then + modules.unload('ta_signal_query') +end --- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +-- Disable RFC8109 priming, scenario doesn't provide expected answers +if priming then + modules.unload('priming') +end +-- Disable this module because it make one priming query +if detect_time_skew then + modules.unload('detect_time_skew') +end + +_hint_root_file('hints') cache.size = 2*MB +verbose(true) +{% endraw %} + +net = { '{{SELF_ADDR}}' } {% if QMIN == "false" %} @@ -14,29 +33,6 @@ option('NO_MINIMIZE', true) option('NO_MINIMIZE', false) {% endif %} -{% if DO_NOT_QUERY_LOCALHOST == "false" %} -option('ALLOW_LOCAL', true) -{% else %} -option('ALLOW_LOCAL', false) -{% endif %} - -{% if HARDEN_GLUE == "true" %} -mode('normal') -{% else %} -mode('permissive') -{% endif %} - -{% for TAF in TRUST_ANCHOR_FILES %} -trust_anchors.add_file('{{TAF}}') -{% endfor %} -trust_anchors.set_insecure({ - -{% for DI in NEGATIVE_TRUST_ANCHORS %} -"{{DI}}", -{% endfor %} -}) - -verbose(true) -- Self-checks on globals assert(help() ~= nil) diff --git a/modules/ta_update/ta_update.test.integr/rfc5011_unsupported_key_rollover.rpl b/modules/ta_update/ta_update.test.integr/rfc5011_unsupported_key_rollover.rpl new file mode 100644 index 0000000000000000000000000000000000000000..cf7dba9e93cc2010efa2e68a8417857ad445b878 --- /dev/null +++ b/modules/ta_update/ta_update.test.integr/rfc5011_unsupported_key_rollover.rpl @@ -0,0 +1,90 @@ + trust-anchor: ". IN DS 13876 8 2 240B81A3498168E9F1FF85F83C24B63994D91D0569D7FB13C87E0D59AA8EB2DD" + val-override-date: "20190313000000" + stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET. + query-minimization: off +CONFIG_END + +SCENARIO_BEGIN RFC 5011 key rollover to unsupported algorhitm. + +; K.ROOT-SERVERS.NET. +RANGE_BEGIN 0 100 + ADDRESS 193.0.14.129 + ADDRESS 2001:7fd::1 +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +. IN NS +SECTION ANSWER +. 518400 IN NS k.root-servers.net. +. 518400 IN RRSIG NS 8 0 518400 20190326123543 20190312123543 13191 . kyHWRA9F6SKNXHKbB/roiZIUYvsQXdRzdTYZBWeiHb2puAug4h8NqdU9 yJwOpW7lzZyQILshzThh1NXueSOyJ7VYqxgAqIMiQ7hTKXvgfPsDPZYK hl05XtUZYmXQO5gdXyeKbcsI/oC4yom3IU7wt81Y18CJnlKmbY4hAf7e aDAluhbL4H9/4dXWyVBNKk8aOzHnusWjbyFdb/+UlGVEv62RhXYYMuWy c1v/8uSc1CHSgS9ef1krVkqstJtaob5lysa6Vko08XTsDKmyUJXrhhgz wzmZKaVpthAM58dMm+Twho+tLpQ2HApZUOa6Z7F9Rc2QnNLMJLRl7Iz+ fq7JBg== +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +. IN DNSKEY +SECTION ANSWER +. 1814400 IN DNSKEY 257 3 253 AwEAAcVR4S9H/xPz0EZNso6tsX+z/CLhzwsDNbPVQNWddu5YP04iHKkA prBuseYVwswkQm14Jqr7u2oLOMDJ0Vn0tbw7UfBDD9nLlMhi8X3l8X++ T7xzqn99xL+8Ad0L5xQwRR7dlij8SuL0DuNhWpWmKwPDP7mI/oTNSYLD 3U/zm023Wgq+mrx+7w9Or7bh9Fo/bPN54RsTQ3BIg7LM2/wmLKtHZqiR lpyCF5gQ+eUSR6JGDfedjrvl+ywEl7dcmF11MV69pyAeASNo9+mvknwx VearXoz1KcNiCpgNmuu3lBQvoygTVmDw0RvYiWkVUmm/b+mo6hsYz6O+ XSRya8C681c= +. 1814400 IN DNSKEY 257 3 8 AwEAAcVR4S9H/xPz0EZNso6tsX+z/CLhzwsDNbPVQNWddu5YP04iHKkA prBuseYVwswkQm14Jqr7u2oLOMDJ0Vn0tbw7UfBDD9nLlMhi8X3l8X++ T7xzqn99xL+8Ad0L5xQwRR7dlij8SuL0DuNhWpWmKwPDP7mI/oTNSYLD 3U/zm023Wgq+mrx+7w9Or7bh9Fo/bPN54RsTQ3BIg7LM2/wmLKtHZqiR lpyCF5gQ+eUSR6JGDfedjrvl+ywEl7dcmF11MV69pyAeASNo9+mvknwx VearXoz1KcNiCpgNmuu3lBQvoygTVmDw0RvYiWkVUmm/b+mo6hsYz6O+ XSRya8C681c= +. 1814400 IN DNSKEY 256 3 8 AwEAAZ7wwdoX/a2Va1Wx5tlTF/gVpznA/m1m7jvhnEjHCVE6iGQW3qII +tL87ScygLKV25ATPmfjIIkIIG7/NSx66eo2KiJusDjzUR8BQWcy/SHd k+r8yCifsIYTaKqgtnj91gYPoY22bG4CUt8/v1hl9FWh+C+X6occdmLr uXxeo6UOhORkM9oVcK2tOLgK1oedarg5z663JmQdEjwPkgYS7QazCAHh m3eQF8n6mD1AqKh1O6uNaVmLh3mvaI2K/0E9jRfefHJgWh1v2PfRtqlG j9idQkBZX+3IclEx8BoSXrRxVdehBvyntS+eqgx/YBOnJcdH1kOls/s2 0ZknTVQvOdE= +. 1209600 IN RRSIG DNSKEY 8 0 1814400 20190402125328 20190312125328 13876 . qy0f6TfZls3/njJKIQlpZC3/Zq7e1O7VUFtEDiDCk8vU23PeARcMNDfb Io9VPE4MqUtHDJ7DuHUlSttLwH4KZUK7uoYW74Ii6YlnE+2pci1lj8Bn PlodQiOAhrpeH6BdZe55La5uGFE/GB7w9vbjVf6ytz5HBrdFUFoxg5V/ vUwnZS12eW0JY8HXZ7kdiyr/z9eOIRmUYIZHTXDzT5MJBYAaoDXBqE0j DKwxTn5Wx5/O2KthiRYc0j44hEQBawQnL0upBRmof+iAuUInoMBrk1u8 Ylr7RSbvS69qs8lkWGPC6VSKvAnludzcTW79K5avz3jST6rccSowuFNI oyN5UA== +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +k.root-servers.net. IN AAAA +SECTION ANSWER +k.root-servers.net. 3600000 IN AAAA 2001:7fd::1 +k.root-servers.net. 1209600 IN RRSIG AAAA 8 3 3600000 20190326123543 20190312123543 13191 . GNsOgKS3KLLHf7J05LRrLHoWWq8qtcyLoT9x5b4fk6yQvSjbRrYxjxsv kUR9f3RE+dPndevDv/GI5PHQ/UKgWeVQEvyhU5QjgveR/AvULIy3dk8f FzkOd/USy931BrOCJF2Zqzw0pHavjotdDVsoVWwwgjNlrahtKFP+e8Bm qD7C1NVrncv9bmMYNlH/ZrPniXR1pWYt4294rrSUSqoH+tVxmwdwX2kL SU/c/a4p+7ST/+GhsG26QBl0K/OJP7nAwdKP1gQBUoKDRUIzXlPdDIc8 fvDaYPq8iKYA5QHAXy3Fvd4Z02J9iTc1/vTncDJata3CNUk2B295f5F/ uk+a0Q== +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +k.root-servers.net. IN A +SECTION ANSWER +k.root-servers.net. 3600000 IN A 193.0.14.129 +k.root-servers.net. 1209600 IN RRSIG A 8 3 3600000 20190326123543 20190312123543 13191 . fi34mMaQ+cEP1mueazJ3YXBOTKX5FGU9hZGQKMogrKLw4jwItTaxBtl2 CYCMP8B2rX9bAhBNjvqxqT5Lj1LJfomKLi+eVQhGONL3t8TgIFml6Z72 7d2qr/AiGgNH71tF/mbf5xFKrIOx37k0is3nRSmbB1FWMuvpVtlPFBey H1rAF/o69jnI7xvFu4TnQHQM+tG/NeCa1fBTJB2J02gS2XUBgPIk9f1a bkuf7nofj7tEN7+jHv2U3dDFDoMafcYIgzF/wlieqDTorBi9SkT68+nh hpJAG29d0rbG3CWUPI6Tm6El8eH+3hC6B8Emc3T30m3R5E/A4VJsbsOt vKBUIQ== +ENTRY_END + +; QTYPE == RRSIG is not supported, https://tools.ietf.org/html/draft-ietf-dnsop-refuse-any-04#section-7 +ENTRY_BEGIN +MATCH opcode qtype +ADJUST copy_id copy_query +REPLY QR AA REFUSED +SECTION QUESTION +. IN RRSIG +ENTRY_END +RANGE_END + +STEP 1 QUERY +ENTRY_BEGIN +REPLY RD DO +SECTION QUESTION +. IN NS +ENTRY_END + + +STEP 10 CHECK_ANSWER +ENTRY_BEGIN +MATCH opcode qname flags rcode question answer +REPLY QR RD RA AD DO NOERROR +SECTION QUESTION +. IN NS +SECTION ANSWER +. 518400 IN NS k.root-servers.net. +. 518400 IN RRSIG NS 8 0 518400 20190326123543 20190312123543 13191 . kyHWRA9F6SKNXHKbB/roiZIUYvsQXdRzdTYZBWeiHb2puAug4h8NqdU9 yJwOpW7lzZyQILshzThh1NXueSOyJ7VYqxgAqIMiQ7hTKXvgfPsDPZYK hl05XtUZYmXQO5gdXyeKbcsI/oC4yom3IU7wt81Y18CJnlKmbY4hAf7e aDAluhbL4H9/4dXWyVBNKk8aOzHnusWjbyFdb/+UlGVEv62RhXYYMuWy c1v/8uSc1CHSgS9ef1krVkqstJtaob5lysa6Vko08XTsDKmyUJXrhhgz wzmZKaVpthAM58dMm+Twho+tLpQ2HApZUOa6Z7F9Rc2QnNLMJLRl7Iz+ fq7JBg== +SECTION AUTHORITY +SECTION ADDITIONAL +ENTRY_END + +SCENARIO_END diff --git a/modules/ta_update/ta_update.test.lua b/modules/ta_update/ta_update.test.lua new file mode 100644 index 0000000000000000000000000000000000000000..02a2aa4a4fa1634079c197c326d8710e8d3f53a8 --- /dev/null +++ b/modules/ta_update/ta_update.test.lua @@ -0,0 +1,79 @@ +-- shorten update interval +ta_update.refresh_time = 0.3 * sec +ta_update.hold_down_time = 0.6 * sec +sleep_time = 0.9 + +-- prevent build-time config from interfering with the test +trust_anchors.remove('.') + +-- count . IN DNSKEY queries +counter = 0 +local function counter_func (state, req) + local answer = req.answer + local qry = req:current() + if answer:qclass() == kres.class.IN + and qry.stype == kres.type.DNSKEY + and kres.dname2wire(qry.sname) == '\0' then + counter = counter + 1 + end + return state +end +policy.add(policy.all(counter_func)) + +local function test_ta_update_vs_trust_anchors_dependency() + ok(ta_update, 'ta_update module is loaded by default') + + assert(counter == 0, 'test init must work') + same(trust_anchors.add_file('root.keys'), nil, 'load managed TA for root zone') + same(trust_anchors.keysets['\0'].managed, true, 'managed TA has managed flag') + same(type(ta_update.tracked['\0'].event), 'number', 'adding managed TA starts tracking') + same(counter, 0, 'TA refresh is only scheduled') + worker.sleep(sleep_time) + ok(counter > 0, 'TA refresh asked for TA DNSKEY after some time') + + same(ta_update.stop('\0'), nil, 'key tracking can be stopped') + same(ta_update.tracked['\0'], nil, 'stopping removed metadata') + same(trust_anchors.keysets['\0'].managed, false, 'now unmanaged TA does not have managed flag') + counter = 0 + worker.sleep(sleep_time) + same(counter, 0, 'stop() actually prevents further TA refreshes') + + ok(modules.unload('ta_update'), 'module can be unloaded') + same(ta_update, nil, 'unloaded module is nil') + + ok(trust_anchors.remove('.'), 'managed root TA can be removed') + same(trust_anchors.keysets['\0'], nil, 'TA removal works') +end + +local function test_unloaded() + boom(trust_anchors.add_file, {'root.keys', false}, 'managed TA cannot be added without ta_update module') + + counter = 0 + same(trust_anchors.add_file('root.keys', true), nil, 'unmanaged TA can be added without ta_update module') + worker.sleep(sleep_time) + ok(counter == 0, 'TA is actually unmanaged') + + ok(trust_anchors.remove('.'), 'unmanaged root TA can be removed') + same(trust_anchors.keysets['\0'], nil, 'TA removal works') + +end + +local function test_reload() + ok(modules.load('ta_update'), 'module can be re-loaded') + same(trust_anchors.add_file('root.keys', false), nil, 'managed TA can be added after loading ta_update module') + same(counter, 0, 'TA refresh is only scheduled') + worker.sleep(sleep_time) + ok(counter > 0, 'TA refresh asked for TA DNSKEY after some time') +end + +local function test_err_inputs() + ok(modules.load('ta_update'), 'make sure module is loaded') + boom(ta_update.start, {'\12nonexistent'}, 'nonexistent TA cannot be tracked') +end + +return { + test_ta_update_vs_trust_anchors_dependency, + test_unloaded, + test_reload, + test_err_inputs, +} diff --git a/modules/view/addr.test.integr/kresd_config.j2 b/modules/view/addr.test.integr/kresd_config.j2 index a92c1423408bfb77dac097b160bef98ae8401488..08a6be20f115ab735fee72ca10653c71bca3ab43 100644 --- a/modules/view/addr.test.integr/kresd_config.j2 +++ b/modules/view/addr.test.integr/kresd_config.j2 @@ -6,7 +6,12 @@ view:addr('127.0.0.0/24', policy.suffix(policy.DENY_MSG("addr 127.0.0.0/24 match policy.add(policy.all(policy.FORWARD('1.2.3.4'))) -- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') + +-- Disable RFC5011 TA update +if ta_update then + modules.unload('ta_update') +end -- Disable RFC8145 signaling, scenario doesn't provide expected answers if ta_signal_query then diff --git a/modules/view/tsig.test.integr/kresd_config.j2 b/modules/view/tsig.test.integr/kresd_config.j2 index 9ce0162c0b1dc10e06ad840a1d3106edeccafeb5..c306989f7ec357ecb72d21685a71e4c4ecad6e8d 100644 --- a/modules/view/tsig.test.integr/kresd_config.j2 +++ b/modules/view/tsig.test.integr/kresd_config.j2 @@ -7,6 +7,11 @@ view:tsig('\8testkey1\0', policy.suffix(policy.DENY_MSG("TSIG key testkey1 match view:tsig('\7testkey\0', policy.suffix(policy.DENY_MSG("TSIG key testkey matched example"),{"\7example\0"})) policy.add(policy.all(policy.FORWARD('1.2.3.4'))) +-- Disable RFC5011 TA update +if ta_update then + modules.unload('ta_update') +end + -- Disable RFC8145 signaling, scenario doesn't provide expected answers if ta_signal_query then modules.unload('ta_signal_query') @@ -23,7 +28,7 @@ if detect_time_skew then end -- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') _hint_root_file('hints') cache.size = 2*MB diff --git a/scripts/kresd-host.lua b/scripts/kresd-host.lua index 9348716af1d83f3807bc227d095939a0521294c1..bfbcbf19e752d2e914d129d1af279038708efb64 100755 --- a/scripts/kresd-host.lua +++ b/scripts/kresd-host.lua @@ -41,10 +41,10 @@ k = 1 while k <= #arg do k = k + 1 table.insert(config, arg[k]) elseif v == '-D' then - table.insert(config, 'trust_anchors.file = "root.keys"') + table.insert(config, 'trust_anchors.add_file("root.keys")') elseif v == '-f' then k = k + 1 - table.insert(config, string.format('trust_anchors.file = "%s"', arg[k])) + table.insert(config, string.format('trust_anchors.add_file("%s")', arg[k])) elseif v == '-v' then verbose = true elseif v == '-d' then diff --git a/scripts/launch-test-instance.sh b/scripts/launch-test-instance.sh deleted file mode 100755 index 8a933285db4c7e8720e44820f4644b62eb1db07d..0000000000000000000000000000000000000000 --- a/scripts/launch-test-instance.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -e -export PATH="/usr/lib/ccache:$PATH" - -PORT=${1:-$((32767+$(dd if=/dev/urandom count=1 2> /dev/null | cksum | cut -d' ' -f1) % 32768))} - -JOBS=$(cat /proc/cpuinfo | grep processor | wc -l) - -WORKDIR=${2:-$(mktemp -d /tmp/knot-resolver.XXXXXX)} - -PREFIX=${PREFIX:-$WORKDIR} make clean - -CFLAGS=${CFLAGS:-"-O2 -g3"} PREFIX=${PREFIX:-$WORKDIR} make -j ${JOBS} V=1 - -PREFIX=${PREFIX:-$WORKDIR} make install - -install -d -m 0700 ${WORKDIR}/run/kresd - -echo "Launching Knot Resolver on port: ${PORT}" -echo "To debug, use:" -echo "dig +dnssec +multi +time=60 +retry=1 -p ${PORT} @::1" - -LD_LIBRARY_PATH=${WORKDIR}/lib ${WORKDIR}/sbin/kresd -a 127.0.0.1#${PORT} -a ::1#${PORT} -v -k ${ROOT_KEY:-/usr/share/dns/root.key} ${WORKDIR}/run/kresd - -if [ "${WORKDIR}" != "${2}" -a "${KEEP_WORKDIR}" != "yes" ]; then - rm -r ${WORKDIR} -fi diff --git a/tests/config/keyfile/bad_args.test.lua b/tests/config/keyfile/bad_args.test.lua deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tests/config/keyfile/load_ta.test.lua b/tests/config/keyfile/load_ta.test.lua deleted file mode 100644 index bfe851b7c6822c2f8033a9325a0b8f58912c3c9b..0000000000000000000000000000000000000000 --- a/tests/config/keyfile/load_ta.test.lua +++ /dev/null @@ -1,37 +0,0 @@ --- test fixtures - --- count warning message, fail with other than allowed message -warn_msg = {} -overriding_msg="[ ta ] warning: overriding previously set trust anchors for ." -warn_msg[overriding_msg] = 0 -function warn(fmt, ...) - msg = string.format(fmt, ...) - if warn_msg[msg] == nil then - fail(string.format("Not allowed warn message: %s", msg)) - else - warn_msg[msg] = warn_msg[msg] + 1 - end -end - --- tests - -boom(trust_anchors.add_file, {'nonwriteable/root.keys', false}, - "Managed trust anchor in non-writeable directory") - -boom(trust_anchors.add_file, {'nonexist.keys', true}, - "Nonexist unmanaged trust anchor file") - -trust_anchors.add_file('root2.keys', true) -trust_anchors.add_file('root1.keys', true) -is(warn_msg[overriding_msg], 1, "Warning message when override trust anchors") - -is(trust_anchors.keysets['\0'][1].key_tag, 19036, - "Loaded KeyTag from root1.keys") - -local function test_loading_from_cmdline() - is(trust_anchors.keysets['\0'][1].key_tag , 20326, - "Loaded KeyTag from cmdline file root2.keys") - is(warn_msg[overriding_msg], 2, "Warning message when override trust anchors") -end - -return {test_loading_from_cmdline} diff --git a/tests/config/keyfile/nonexist1.test.lua b/tests/config/keyfile/nonexist1.test.lua deleted file mode 100644 index 332919dabbf5dacf692fdc62613505de487d01d5..0000000000000000000000000000000000000000 --- a/tests/config/keyfile/nonexist1.test.lua +++ /dev/null @@ -1,2 +0,0 @@ --- simulate building without keyfile_default -trust_anchors.keyfile_default = nil diff --git a/tests/config/keyfile/nonexist2.test.lua b/tests/config/keyfile/nonexist2.test.lua deleted file mode 100644 index e801c6d3269caa42eb42766dad1f4a7770b0b676..0000000000000000000000000000000000000000 --- a/tests/config/keyfile/nonexist2.test.lua +++ /dev/null @@ -1,2 +0,0 @@ --- simulate building with keyfile_default -trust_anchors.keyfile_default = "../../../../tests/config/keyfile/root1.keys" diff --git a/tests/config/keyfile/root1.keys b/tests/config/keyfile/root1.keys deleted file mode 100644 index c7343371b9f01ac3ca7540d75044e02a56c86350..0000000000000000000000000000000000000000 --- a/tests/config/keyfile/root1.keys +++ /dev/null @@ -1 +0,0 @@ -. 172800 DNSKEY 257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjFFVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoXbfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaDX6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpzW5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relSQageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulqQxA+Uk1ihz0= ; Valid: ; KeyTag:19036 diff --git a/tests/config/keyfile/root2.keys b/tests/config/keyfile/root2.keys deleted file mode 100644 index 5e9d6ac6e0e3964863dd4ea98280e1eb1fb5862e..0000000000000000000000000000000000000000 --- a/tests/config/keyfile/root2.keys +++ /dev/null @@ -1 +0,0 @@ -. 172800 DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU= ; Valid: ; KeyTag:20326 diff --git a/tests/config/meson.build b/tests/config/meson.build index 7d9a68b0b09356b731fd272fe4898fea64bea0d4..c0dff6a242495b6e11527d3679b497aaa4e49585 100644 --- a/tests/config/meson.build +++ b/tests/config/meson.build @@ -1,22 +1,6 @@ config_tests += [ - ['basic', files('basic.test.lua'), [], false, ['skip_asan']], - ['cache', files('cache.test.lua'), [], false, ['skip_asan']], - ['keyfile.bad_args', files('keyfile/bad_args.test.lua'), - ['--keyfile-ro', 'root.keys', - '--keyfile', 'root.keys'], - true, - ], - ['keyfile.load_ta', files('keyfile/load_ta.test.lua'), - ['--keyfile-ro', files('keyfile/root2.keys')] - ], - ['keyfile.nonexist1', files('keyfile/nonexist1.test.lua'), - ['--keyfile-ro', 'nonexist'], - true, - ], - ['keyfile.nonexist2', files('keyfile/nonexist2.test.lua'), - ['--keyfile-ro', 'nonexist'], - true, - ], + ['basic', files('basic.test.lua'), ['skip_asan']], + ['cache', files('cache.test.lua'), ['skip_asan']], ['lru', files('lru.test.lua')], ['tls', files('tls.test.lua')], ['worker', files('worker.test.lua')], @@ -27,20 +11,8 @@ run_configtest = find_program('../../scripts/test-config.sh') foreach config_test : config_tests - # kresd arguments - conftest_args = [ - '-c', files('test.cfg'), - '-f', '1', - ] - if config_test.length() >= 3 - conftest_args += config_test[2] - endif - - # kresd return code check - conftest_should_fail = config_test.length() >= 4 ? config_test[3] : false - # additional suites - extra_suites = config_test.length() >= 5 ? config_test[4] : [] + extra_suites = config_test.length() >= 3 ? config_test[2] : [] # environment variables for test conftest_env = environment() @@ -53,12 +25,14 @@ foreach config_test : config_tests test( 'config.' + config_test[0], run_configtest, - args: conftest_args, + args: [ + '-c', files('test.cfg'), + '-f', '1', + ], env: conftest_env, suite: [ 'postinstall', 'config', ] + extra_suites, - should_fail: conftest_should_fail, ) endforeach diff --git a/tests/integration/deckard b/tests/integration/deckard index 04dd33b568b173ad7e6c01604f61999b091191d1..6edfef4f65e846f3aef0cbf3b80e88f39658a1c9 160000 --- a/tests/integration/deckard +++ b/tests/integration/deckard @@ -1 +1 @@ -Subproject commit 04dd33b568b173ad7e6c01604f61999b091191d1 +Subproject commit 6edfef4f65e846f3aef0cbf3b80e88f39658a1c9 diff --git a/tests/pytests/templates/kresd.conf.j2 b/tests/pytests/templates/kresd.conf.j2 index 141f4389a66d47372a81b9601288a5abb146ca52..72f0ee1a7f9262a97018a77f525d418792e55fd6 100644 --- a/tests/pytests/templates/kresd.conf.j2 +++ b/tests/pytests/templates/kresd.conf.j2 @@ -43,7 +43,8 @@ policy.add(policy.suffix(policy.PASS, {todname('test.')})) {% endif %} -- make sure DNSSEC is turned off for tests -trust_anchors.keyfile_default = nil +trust_anchors.remove('.') +modules.unload("ta_update") modules.unload("ta_signal_query") modules.unload("priming") modules.unload("detect_time_skew")