]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
trust_anchors: use cleaner interface between ta_update and trust_anchors module
authorTomas Krizek <tomas.krizek@nic.cz>
Tue, 19 Mar 2019 13:10:27 +0000 (14:10 +0100)
committerPetr Špaček <petr.spacek@nic.cz>
Thu, 4 Apr 2019 12:18:57 +0000 (14:18 +0200)
+ tests

Exracting RFC 5011 to separate module was a good opportunity for
cleanup.

.gitlab-ci.yml
daemon/lua/trust_anchors.lua.in
modules/meson.build
modules/ta_update/root.key [new file with mode: 0644]
modules/ta_update/ta_update.lua
modules/ta_update/ta_update.test.lua [new file with mode: 0644]

index 43d3d432f0074c3c49b9f409e67d01192336c446..a2cc0f946aae537f3a31fa4ce93c935e0729157d 100644 (file)
@@ -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:
index 5990986531aafd62ad07b5a0102b2035a8607356..e77cc3a6a006b9de8598da1568ca4e3ff1f93e69 100644 (file)
@@ -11,6 +11,7 @@ local key_state = {
        Missing = 'Missing', Revoked = 'Revoked', Removed = 'Removed'
 }
 
+-- 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')
@@ -153,11 +154,20 @@ local function bootstrap(url, ca)
        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:'
+                       .. '[ ta ] Bootstrapped root trust anchors are:'
                        .. rrset
        return rrset, msg
 end
 
+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.
 local function ta_rr_str(ta)
@@ -172,7 +182,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
@@ -298,14 +310,6 @@ local function keyset_publish(keyset)
        return count > 0 and not has_error
 end
 
-local refresh_plan = function(keyset, delay, is_initial)
-       event.after(0, function()
-               if ta_update ~= nil then
-                       ta_update.refresh_plan(keyset, delay, is_initial)
-               end
-       end)
-end
-
 local function add_file(path, unmanaged)
        local managed = not unmanaged
        if managed then
@@ -315,7 +319,7 @@ local function add_file(path, unmanaged)
                os.remove(path .. ".lock")
        end
 
-       -- Bootstrap if requested and keyfile doesn't exist
+       -- 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(
@@ -323,25 +327,15 @@ local function add_file(path, unmanaged)
                                .. "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
-               keyset.managed = true
-               keyset_write(keyset)
-               if keyset.refresh_ev then event.cancel(keyset.refresh_ev) end
-               refresh_plan(keyset, 0, true)
-               return
-       end
-       if managed 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
@@ -349,14 +343,13 @@ local function add_file(path, unmanaged)
        if not keyset then
                panic("[ ta ] ERROR: failed to read anchors from '%s' (%s)", path, err)
        end
-       if managed 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
                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
        end
        trust_anchors.keysets[owner] = keyset
 
@@ -367,7 +360,13 @@ local function add_file(path, unmanaged)
        end
        -- TODO: if failed and for root, try to rebootstrap?
 
-       if managed then refresh_plan(keyset, 0 * sec, false) end
+       if managed then
+               if not ta_update then
+                       panic('[ ta ] automatic update for TA ' .. owner_str .. ' requested, '
+                               .. 'but required plugin ta_update is not loaded')
+               end
+               ta_update.start(owner)
+       end
 end
 
 local function distrust(owner)
@@ -431,8 +430,9 @@ trust_anchors = {
        config = add_file,
        distrust = distrust,
 
-       keyset_write = keyset_write,
        keyset_publish = keyset_publish,
+       keyset_write = keyset_write,
+       key_state = key_state,
 
        -- Add DS/DNSKEY record(s) (unmanaged)
        add = function (keystr)
@@ -443,15 +443,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
@@ -459,6 +459,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
index dcadcbd82a377f7afe641794ed13a0eff199bdc5..5df7fedd8ca0419ae5936139ea822b084baeffac 100644 (file)
@@ -23,6 +23,7 @@ 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 += [
diff --git a/modules/ta_update/root.key b/modules/ta_update/root.key
new file mode 100644 (file)
index 0000000..3836b31
--- /dev/null
@@ -0,0 +1 @@
+.                      163395  IN      DNSKEY  257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU=
index 68907e742aec99f9f26787edcd166cc8b1eb2968..3d30b64bfe3f1481c86a92455ce4f58734c12d6d 100644 (file)
@@ -3,13 +3,13 @@ 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}
 
--- 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)
@@ -229,10 +229,19 @@ local function active_refresh(keyset, pkt, is_initial)
        return math.max(hour, min_ttl)
 end
 
--- Plan an event for refreshing the root DNSKEYs and re-scheduling itself
+-- Plan an event for refreshing 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 ()
+       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
@@ -251,7 +260,45 @@ ta_update = {
        hold_down_time = 30 * day,
        refresh_time = nil,
        keep_removed = 0,
-       refresh_plan = refresh_plan,
+       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; distrust it and '
+                       .. 'add it again as managed using trust_anchors.add_file()')
+       end
+       refresh_plan(keyset, 0, false)
+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
+
+-- immediatelly schedule key refresh for all managed TAs
+function ta_update.init()
+       for zname, keyset in pairs(trust_anchors.keysets) do
+               if keyset.managed then
+                       ta_update.start(zname)
+               end
+       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.lua b/modules/ta_update/ta_update.test.lua
new file mode 100644 (file)
index 0000000..a986c88
--- /dev/null
@@ -0,0 +1,78 @@
+-- shorten update interval to 0.1 seconds
+ta_update.refresh_time = 0.1 * sec
+ta_update.hold_down_time = 0.2 * sec
+
+-- prevent build-time config from interfering with the test
+trust_anchors.keyfile_default = nil
+
+-- 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.config('root.key'), 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(0.3)
+       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(0.3)
+       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.distrust('\0'), 'managed root TA can be removed')
+       same(trust_anchors.keysets['\0'], nil, 'TA removal works')
+end
+
+local function test_unloaded()
+       boom(trust_anchors.config, {'root.key', false}, 'managed TA cannot be added without ta_update module')
+
+       counter = 0
+       same(trust_anchors.config('root.key', true), nil, 'unmanaged TA can be added without ta_update module')
+       worker.sleep(0.3)
+       ok(counter == 0, 'TA is actually unmanaged')
+
+       ok(trust_anchors.distrust('\0'), '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.config('root.key', false), nil, 'managed TA can be added after loading ta_update module')
+       same(counter, 0, 'TA refresh is only scheduled')
+       worker.sleep(0.3)
+       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,
+}