Commit 22b0c6d5 authored by Tomas Krizek's avatar Tomas Krizek Committed by Petr Špaček

trust_anchors: use cleaner interface between ta_update and trust_anchors module

+ tests

Exracting RFC 5011 to separate module was a good opportunity for
cleanup.
parent 0ca663dc
......@@ -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:
......
......@@ -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
......
......@@ -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 += [
......
. 163395 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU=
......@@ -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
-- 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,
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment