diff --git a/daemon/lua/trust_anchors.lua b/daemon/lua/trust_anchors.lua index 7f3c287bc8887c66ff96eb1ea597e80809e24838..cfc7a9ecc55b2c89354f48a38d31862898f39159 100644 --- a/daemon/lua/trust_anchors.lua +++ b/daemon/lua/trust_anchors.lua @@ -1,9 +1,6 @@ local kres = require('kres') local C = require('ffi').C --- Add or remove hold-down timer -local hold_down_time = 30 * day - -- RFC5011 state table local key_state = { Start = 'Start', AddPend = 'AddPend', Valid = 'Valid', @@ -16,7 +13,7 @@ local function ta_find(keyset, rr) local ta = keyset[i] -- Match key owner and content if ta.owner == rr.owner and - C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) then + C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) == 0 then return ta end end @@ -24,10 +21,10 @@ local function ta_find(keyset, rr) end -- Evaluate TA status according to RFC5011 -local function ta_present(keyset, rr, force) +local function ta_present(keyset, rr, hold_down_time, force) if not C.kr_dnssec_key_ksk(rr.rdata) then return false -- Ignore - end + end -- Find the key in current key set and check its status local now = os.time() local key_revoked = C.kr_dnssec_key_revoked(rr.rdata) @@ -59,6 +56,7 @@ local function ta_present(keyset, rr, force) print('[trust_anchors] key: '..key_tag..' state: '..ta.state) return true elseif not key_revoked then -- First time seen (NewKey) + rr.key_tag = key_tag if force then rr.state = key_state.Valid else @@ -73,7 +71,7 @@ local function ta_present(keyset, rr, force) end -- TA is missing in the new key set -local function ta_missing(keyset, ta) +local function ta_missing(keyset, 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) @@ -89,24 +87,79 @@ local function ta_missing(keyset, ta) return keep_ta end +-- Plan refresh event and re-schedule itself based on the result of the callback +local function refresh_plan(trust_anchors, timeout, refresh_cb) + if trust_anchors.refresh_ev ~= nil then event.cancel(trust_anchors.refresh_ev) end + trust_anchors.refresh_ev = event.after(timeout, function (ev) + worker.resolve('.', kres.type.DNSKEY, kres.class.IN, kres.query.NO_CACHE, + function (pkt) + -- Schedule itself with updated timeout + local next_time = refresh_cb(trust_anchors, kres.pkt_t(pkt)) + next_time = math.min(next_time, trust_anchors.refresh_time) + print('[trust_anchors] next refresh: '..next_time) + refresh_plan(trust_anchors, next_time, refresh_cb) + end) + end) +end + +-- Active refresh, return time of the next check +local function active_refresh(trust_anchors, pkt) + local retry = true + if pkt:rcode() == kres.rcode.NOERROR then + local records = pkt:section(kres.section.ANSWER) + local keyset = {} + for i = 1, #records do + local rr = records[i] + if rr.type == kres.type.DNSKEY then + table.insert(keyset, rr) + end + end + trust_anchors.update(keyset, false) + retry = false + end + -- Calculate refresh/retry timer (RFC 5011, 2.3) + local min_ttl = retry and day or 15 * day + for i, rr in ipairs(trust_anchors.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 + +-- Write keyset to a file +local function keyset_write(keyset, path) + local file = assert(io.open(path..'.lock', 'w')) + for i = 1, #keyset do + local ta = keyset[i] + local rr_str = string.format('%s ; %s\n', kres.rr2str(ta), ta.state) + if ta.state ~= key_state.Valid and ta.state ~= key_state.Missing then + rr_str = '; '..rr_str -- Invalidate key string + end + file:write(rr_str) + end + file:close() + os.rename(path..'.lock', path) +end + -- TA store management local trust_anchors = { keyset = {}, insecure = {}, + hold_down_time = 30 * day, -- Update existing keyset update = function (new_keys, initial) if not new_keys then return false end -- Filter TAs to be purged from the keyset (KeyRem) + local hold_down = trust_anchors.hold_down_time / 1000 local keyset_keep = {} local keyset = trust_anchors.keyset for i = 1, #keyset do local ta = keyset[i] local keep = true if not ta_find(new_keys, ta) then - keep = ta_missing(keyset, ta) + keep = ta_missing(trust_anchors, keyset, ta, hold_down) end if keep then - table.insert(keyset_keep, rr) + table.insert(keyset_keep, ta) end end keyset = keyset_keep @@ -114,12 +167,13 @@ local trust_anchors = { for i = 1, #new_keys do local rr = new_keys[i] if rr.type == kres.type.DNSKEY then - ta_present(keyset, rr, initial) + ta_present(keyset, rr, hold_down, initial) end end -- Publish active TAs local store = kres.context().trust_anchors C.kr_ta_clear(store) + if #keyset == 0 then return false end for i = 1, #keyset do local ta = keyset[i] -- Key MAY be used as a TA only in these two states (RFC5011, 4.2) @@ -128,12 +182,21 @@ local trust_anchors = { end end trust_anchors.keyset = keyset + -- Store keyset in the file + if trust_anchors.file_current ~= nil then + keyset_write(keyset, trust_anchors.file_current) + end return true end, -- Load keys from a file (managed) - config = function (path) + config = function (path, is_unmanaged) local new_keys = require('zonefile').parse_file(path) - trust_anchors.update(new_keys, true) + trust_anchors.file_current = path + if is_unmanaged then trust_anchors.file_current = nil end + trust_anchors.keyset = {} + if trust_anchors.update(new_keys, true) then + refresh_plan(trust_anchors, sec, active_refresh) + end end, -- Add DS/DNSKEY record(s) (unmanaged) add = function (keystr)