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: ...@@ -302,7 +302,7 @@ root.hints:
- scripts/update-root-hints.sh - scripts/update-root-hints.sh
test:valgrind: test:valgrind:
<<: *test <<: *test_flaky # lost block in /bin/bash during ta_update
when: delayed when: delayed
start_in: '30 seconds' start_in: '30 seconds'
script: script:
......
...@@ -11,6 +11,7 @@ local key_state = { ...@@ -11,6 +11,7 @@ local key_state = {
Missing = 'Missing', Revoked = 'Revoked', Removed = 'Removed' 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 -- Fetch over HTTPS with peert cert checked
local function https_fetch(url, ca) local function https_fetch(url, ca)
local ssl_ok, https = pcall(require, 'ssl.https') local ssl_ok, https = pcall(require, 'ssl.https')
...@@ -153,11 +154,20 @@ local function bootstrap(url, ca) ...@@ -153,11 +154,20 @@ local function bootstrap(url, ca)
local msg = '[ ta ] Root trust anchors bootstrapped over https with pinned certificate.\n' local msg = '[ ta ] Root trust anchors bootstrapped over https with pinned certificate.\n'
.. ' You SHOULD verify them manually against original source:\n' .. ' You SHOULD verify them manually against original source:\n'
.. ' https://www.iana.org/dnssec/files\n' .. ' https://www.iana.org/dnssec/files\n'
.. '[ ta ] Current root trust anchors are:' .. '[ ta ] Bootstrapped root trust anchors are:'
.. rrset .. rrset
return rrset, msg return rrset, msg
end 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 -- Update ta.comment and return decorated line representing the RR
-- This is meant to be in zone-file format. -- This is meant to be in zone-file format.
local function ta_rr_str(ta) local function ta_rr_str(ta)
...@@ -172,7 +182,9 @@ end ...@@ -172,7 +182,9 @@ end
-- Write keyset to a file. States and timers are stored in comments. -- Write keyset to a file. States and timers are stored in comments.
local function keyset_write(keyset) 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 fname_tmp = keyset.filename .. '.lock.' .. tostring(worker.pid);
local file = assert(io.open(fname_tmp, 'w')) local file = assert(io.open(fname_tmp, 'w'))
for i = 1, #keyset do for i = 1, #keyset do
...@@ -298,14 +310,6 @@ local function keyset_publish(keyset) ...@@ -298,14 +310,6 @@ local function keyset_publish(keyset)
return count > 0 and not has_error return count > 0 and not has_error
end 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 function add_file(path, unmanaged)
local managed = not unmanaged local managed = not unmanaged
if managed then if managed then
...@@ -315,7 +319,7 @@ local function add_file(path, unmanaged) ...@@ -315,7 +319,7 @@ local function add_file(path, unmanaged)
os.remove(path .. ".lock") os.remove(path .. ".lock")
end 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 managed and not io.open(path, 'r') then
if trust_anchors.keysets['\0'] then if trust_anchors.keysets['\0'] then
error(string.format( error(string.format(
...@@ -323,25 +327,15 @@ local function add_file(path, unmanaged) ...@@ -323,25 +327,15 @@ local function add_file(path, unmanaged)
.. "cannot bootstrap; provide a path to valid file with keys", path)) .. "cannot bootstrap; provide a path to valid file with keys", path))
end end
log("[ ta ] keyfile '%s': doesn't exist, bootstrapping", path); log("[ ta ] keyfile '%s': doesn't exist, bootstrapping", path);
local tas, msg = bootstrap(trust_anchors.bootstrap_url, trust_anchors.bootstrap_ca) local rrstr, msg = bootstrap(trust_anchors.bootstrap_url, trust_anchors.bootstrap_ca)
if not tas then if not rrstr then
msg = msg .. '\n' msg = msg .. '\n'
.. '[ ta ] Failed to bootstrap root trust anchors!' .. '[ ta ] Failed to bootstrap root trust anchors!'
error(msg) error(msg)
end end
print(msg) print(msg)
trust_anchors.add(tas) bootstrap_write(rrstr, path)
-- Fetch DNSKEY immediately -- continue as if the keyfile was there
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
end end
-- Parse the file and check its sanity -- Parse the file and check its sanity
...@@ -349,14 +343,13 @@ local function add_file(path, unmanaged) ...@@ -349,14 +343,13 @@ local function add_file(path, unmanaged)
if not keyset then if not keyset then
panic("[ ta ] ERROR: failed to read anchors from '%s' (%s)", path, err) panic("[ ta ] ERROR: failed to read anchors from '%s' (%s)", path, err)
end end
if managed then keyset.filename = path end keyset.filename = path
keyset.managed = managed
local owner = keyset.owner local owner = keyset.owner
local owner_str = kres.dname2str(owner) local owner_str = kres.dname2str(owner)
if trust_anchors.keysets[owner] then if trust_anchors.keysets[owner] then
warn('[ ta ] warning: overriding previously set trust anchors for ' .. owner_str) 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 end
trust_anchors.keysets[owner] = keyset trust_anchors.keysets[owner] = keyset
...@@ -367,7 +360,13 @@ local function add_file(path, unmanaged) ...@@ -367,7 +360,13 @@ local function add_file(path, unmanaged)
end end
-- TODO: if failed and for root, try to rebootstrap? -- 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 end
local function distrust(owner) local function distrust(owner)
...@@ -431,8 +430,9 @@ trust_anchors = { ...@@ -431,8 +430,9 @@ trust_anchors = {
config = add_file, config = add_file,
distrust = distrust, distrust = distrust,
keyset_write = keyset_write,
keyset_publish = keyset_publish, keyset_publish = keyset_publish,
keyset_write = keyset_write,
key_state = key_state,
-- Add DS/DNSKEY record(s) (unmanaged) -- Add DS/DNSKEY record(s) (unmanaged)
add = function (keystr) add = function (keystr)
...@@ -443,15 +443,15 @@ trust_anchors = { ...@@ -443,15 +443,15 @@ trust_anchors = {
local keyset_orig = trust_anchors.keysets[owner] local keyset_orig = trust_anchors.keysets[owner]
-- Set up trust_anchors.keysets[owner] -- Set up trust_anchors.keysets[owner]
if keyset_orig then 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 ' warn('[ ta ] warning: extending previously set trust anchors for '
.. owner_str) .. owner_str)
for _, ta in ipairs(keyset) do for _, ta in ipairs(keyset) do
table.insert(keyset_orig, ta) table.insert(keyset_orig, ta)
end 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 end
-- Replace the TA store used for validation -- Replace the TA store used for validation
if not keyset_publish(keyset) then if not keyset_publish(keyset) then
...@@ -459,6 +459,9 @@ trust_anchors = { ...@@ -459,6 +459,9 @@ trust_anchors = {
-- trust_anchors.keysets[owner] was already updated to the -- trust_anchors.keysets[owner] was already updated to the
-- (partially) failing state, but I'm not sure how much to improve this -- (partially) failing state, but I'm not sure how much to improve this
end end
keyset.managed = false
trust_anchors.keysets[owner] = keyset
end end
if verbose() or err then log('New TA state:\n' .. trust_anchors.summary()) end if verbose() or err then log('New TA state:\n' .. trust_anchors.summary()) end
if err then if err then
......
...@@ -23,6 +23,7 @@ config_tests += [ ...@@ -23,6 +23,7 @@ config_tests += [
['hints', files('hints/tests/hints.test.lua')], ['hints', files('hints/tests/hints.test.lua')],
['nsid', files('nsid/nsid.test.lua')], ['nsid', files('nsid/nsid.test.lua')],
['dns64', files('dns64/dns64.test.lua')], ['dns64', files('dns64/dns64.test.lua')],
['ta_update', files('ta_update/ta_update.test.lua')],
] ]
integr_tests += [ 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') ...@@ -3,13 +3,13 @@ local ffi = require('ffi')
local kres = require('kres') local kres = require('kres')
local C = ffi.C 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 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 -- Find key in current keyset
local function ta_find(keyset, rr) local function ta_find(keyset, rr)
...@@ -229,10 +229,19 @@ local function active_refresh(keyset, pkt, is_initial) ...@@ -229,10 +229,19 @@ local function active_refresh(keyset, pkt, is_initial)
return math.max(hour, min_ttl) return math.max(hour, min_ttl)
end 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 function refresh_plan(keyset, delay, is_initial)
local owner_str = kres.dname2str(keyset.owner) -- maybe fix converting back and forth? local owner = keyset.owner
keyset.refresh_ev = event.after(delay, function () 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', resolve(owner_str, kres.type.DNSKEY, kres.class.IN, 'NO_CACHE',
function (pkt) function (pkt)
-- Schedule itself with updated timeout -- Schedule itself with updated timeout
...@@ -251,7 +260,45 @@ ta_update = { ...@@ -251,7 +260,45 @@ ta_update = {
hold_down_time = 30 * day, hold_down_time = 30 * day,
refresh_time = nil, refresh_time = nil,
keep_removed = 0, 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 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