-- SPDX-License-Identifier: GPL-3.0-or-later -- Module interface 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} -- Find key in current keyset local function ta_find(keyset, rr) local rr_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata) if rr_tag < 0 or rr_tag > 65535 then log_warn(ffi.C.LOG_GRP_TAUPDATE, string.format('ignoring invalid or unsupported RR: %s: %s', kres.rr2str(rr), ffi.string(C.knot_strerror(rr_tag)))) return nil end for i, ta in ipairs(keyset) do -- Match key owner and content local ta_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata) if ta_tag < 0 or ta_tag > 65535 then log_warn(ffi.C.LOG_GRP_TAUPDATE, string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s', kres.rr2str(ta), ffi.string(C.knot_strerror(ta_tag)))) else if ta.owner == rr.owner then if ta.type == rr.type then if rr.type == kres.type.DNSKEY then if C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) == 0 then return ta end elseif rr.type == kres.type.DS and ta.rdata == rr.rdata then return ta end -- DNSKEY superseding DS, inexact match elseif rr.type == kres.type.DNSKEY and ta.type == kres.type.DS then if ta.key_tag == rr_tag then keyset[i] = rr -- Replace current DS rr.state = ta.state rr.key_tag = ta.key_tag return rr end -- DS key matching DNSKEY, inexact match elseif rr.type == kres.type.DS and ta.type == kres.type.DNSKEY then if rr_tag == ta_tag then return ta end end end end end return nil end -- Evaluate TA status of a RR according to RFC5011. The time is in seconds. local function ta_present(keyset, rr, hold_down_time) if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then return false -- Ignore end -- Attempt to extract key_tag local key_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata) if key_tag < 0 or key_tag > 65535 then log_warn(ffi.C.LOG_GRP_TAUPDATE, string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s', kres.rr2str(rr), ffi.string(C.knot_strerror(key_tag)))) return false end -- Find the key in current key set and check its status local now = os.time() local key_revoked = (rr.type == kres.type.DNSKEY) and C.kr_dnssec_key_revoked(rr.rdata) local ta = ta_find(keyset, rr) if ta then -- Key reappears (KeyPres) if ta.state == key_state.Missing then ta.state = key_state.Valid ta.timer = nil end -- Key is revoked (RevBit) if ta.state == key_state.Valid or ta.state == key_state.Missing then if key_revoked then ta.state = key_state.Revoked ta.timer = now + hold_down_time end end -- Remove hold-down timer expires (RemTime) if ta.state == key_state.Revoked and os.difftime(ta.timer, now) <= 0 then ta.state = key_state.Removed ta.timer = nil end -- Add hold-down timer expires (AddTime) if ta.state == key_state.AddPend and os.difftime(ta.timer, now) <= 0 then ta.state = key_state.Valid ta.timer = nil end if rr.state ~= key_state.Valid then log_info(ffi.C.LOG_GRP_TAUPDATE, 'key: ' .. key_tag .. ' state: '..ta.state) end return true elseif not key_revoked then -- First time seen (NewKey) rr.state = key_state.AddPend rr.key_tag = key_tag rr.timer = now + hold_down_time table.insert(keyset, rr) return false end end -- TA is missing in the new key set. The time is in seconds. local function ta_missing(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) if key_tag < 0 or key_tag > 65535 then log_warn(ffi.C.LOG_GRP_TAUPDATE, string.format('[ta_update] ignoring invalid or unsupported RR: %s: %s', kres.rr2str(ta), ffi.string(C.knot_strerror(key_tag)))) key_tag = '' end if ta.state == key_state.Valid then ta.state = key_state.Missing ta.timer = os.time() + hold_down_time -- Remove key that is missing for too long elseif ta.state == key_state.Missing and os.difftime(ta.timer, os.time()) <= 0 then ta.state = key_state.Removed log_info(ffi.C.LOG_GRP_TAUPDATE, 'key: '..key_tag..' removed because missing for too long') keep_ta = false -- Purge pending key elseif ta.state == key_state.AddPend then log_info(ffi.C.LOG_GRP_TAUPDATE, 'key: '..key_tag..' purging') keep_ta = false end log_info(ffi.C.LOG_GRP_TAUPDATE, 'key: '..key_tag..' state: '..ta.state) return keep_ta end -- Update existing keyset; return true if successful. local function update(keyset, new_keys) if not new_keys then return false end if not keyset.managed then -- this may happen due to race condition during testing in CI (refresh time < query time) return false end -- Filter TAs to be purged from the keyset (KeyRem), in three steps -- 1: copy TAs to be kept to `keepset` local hold_down = (keyset.hold_down_time or ta_update.hold_down_time) / 1000 local keepset = {} local keep_removed = keyset.keep_removed or ta_update.keep_removed for _, ta in ipairs(keyset) do local keep = true if not ta_find(new_keys, ta) then -- Ad-hoc: RFC 5011 doesn't mention removing a Missing key. -- Let's do it after a very long period has elapsed. keep = ta_missing(ta, hold_down * 4) end -- Purge removed keys if ta.state == key_state.Removed then if keep_removed > 0 then keep_removed = keep_removed - 1 else keep = false end end if keep then table.insert(keepset, ta) end end -- 2: remove all TAs - other settings etc. will remain in the keyset for i, _ in ipairs(keyset) do keyset[i] = nil end -- 3: move TAs to be kept into the keyset (same indices) for k, ta in pairs(keepset) do keyset[k] = ta end -- Evaluate new TAs for _, rr in ipairs(new_keys) do if (rr.type == kres.type.DNSKEY or rr.type == kres.type.DS) and rr.rdata ~= nil then ta_present(keyset, rr, hold_down) end end -- Store the keyset trust_anchors.keyset_write(keyset) -- Start using the new TAs. if not trust_anchors.keyset_publish(keyset) then -- TODO: try to rebootstrap if for root? return false else log_debug(ffi.C.LOG_GRP_TAUPDATE, 'refreshed trust anchors for domain ' .. kres.dname2str(keyset.owner) .. ' are:\n' .. trust_anchors.summary(keyset.owner)) end return true end local function unmanagedkey_change(file_name) log_warn(ffi.C.LOG_GRP_TAUPDATE, 'you need to update package with trust anchors in "%s" before it breaks', file_name) end local function check_upstream(keyset, new_keys) local process_keys = {} for _, rr in ipairs(new_keys) do local key_revoked = (rr.type == kres.type.DNSKEY) and C.kr_dnssec_key_revoked(rr.rdata) local ta = ta_find(keyset, rr) table.insert(process_keys, ta) if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then goto continue -- Ignore end if not ta and not key_revoked then -- I see new key ta_update.cb_unmanagedkey_change(keyset.filename) end if ta and key_revoked then -- I see revoked key ta_update.cb_unmanagedkey_change(keyset.filename) end ::continue:: end for _, rr in ipairs(keyset) do local missing_rr = true for _, rr_old in ipairs(process_keys) do if (rr.owner == rr_old.owner) and (rr.type == rr_old.type) and (rr.type == kres.type.DNSKEY) then if C.kr_dnssec_key_match(rr.rdata, #rr.rdata, rr_old.rdata, #rr_old.rdata) == 0 then missing_rr = false break end end end if missing_rr then -- This key is missing in the new keyset ta_update.cb_unmanagedkey_change(keyset.filename) end end end -- Refresh the DNSKEYs from the packet, and return time to the next check. local function active_refresh(keyset, pkt, req, managed) local retry = true if pkt ~= nil and pkt:rcode() == kres.rcode.NOERROR then local records = pkt:section(kres.section.ANSWER) local new_keys = {} for _, rr in ipairs(records) do if rr.type == kres.type.DNSKEY then table.insert(new_keys, rr) end end if managed then update(keyset, new_keys) else check_upstream(keyset, new_keys) end retry = false else local qry = req:initial() if qry.flags.DNSSEC_BOGUS == true then log_warn(ffi.C.LOG_GRP_TAUPDATE, 'active refresh failed, update your trust anchors in "%s"', keyset.filename) elseif pkt == nil then log_warn(ffi.C.LOG_GRP_TAUPDATE, 'active refresh failed, answer was dropped') else log_warn(ffi.C.LOG_GRP_TAUPDATE, 'active refresh failed for ' .. kres.dname2str(keyset.owner) .. ' with rcode: ' .. pkt:rcode()) end end -- Calculate refresh/retry timer (RFC 5011, 2.3) local min_ttl = retry and day or 15 * day for _, rr in ipairs(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 -- Plan an event for refreshing DNSKEYs and re-scheduling itself local function refresh_plan(keyset, delay, managed) 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_info(ffi.C.LOG_GRP_TAUPDATE, 'refreshing TA for ' .. owner_str) resolve(owner_str, kres.type.DNSKEY, kres.class.IN, 'NO_CACHE', function (pkt, req) -- Schedule itself with updated timeout local delay_new = active_refresh(keyset, pkt, req, managed) delay_new = keyset.refresh_time or ta_update.refresh_time or delay_new log_info(ffi.C.LOG_GRP_TAUPDATE, 'next refresh for ' .. owner_str .. ' in ' .. delay_new/hour .. ' hours') refresh_plan(keyset, delay_new, managed) end) end) end ta_update = { -- [optional] overrides for global defaults of -- hold_down_time, refresh_time, keep_removed hold_down_time = 30 * day, refresh_time = nil, keep_removed = 0, tracked = tracked_tas, -- debug and visibility, should not be changed by hand cb_unmanagedkey_change = unmanagedkey_change, } -- start tracking (already loaded) TA with given zone name in wire format -- do first refresh immediately function ta_update.start(zname, managed) local keyset = trust_anchors.keysets[zname] if not keyset then panic('[ta_update] TA must be configured first before tracking it') end refresh_plan(keyset, 0, managed) 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 -- stop all timers function ta_update.deinit() for zname, _ in pairs(tracked_tas) do ta_update.stop(zname) end end return ta_update