- 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:
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')
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)
-- 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
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
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(
.. "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
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
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)
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)
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
-- 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
['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 += [
--- /dev/null
+. 163395 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU=
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)
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
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
--- /dev/null
+-- 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,
+}