1
0
Fork 0
knot-resolver/modules/ta_update/ta_update.lua
Daniel Baumann fbc604e215
Adding upstream version 5.7.5.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-21 13:56:17 +02:00

349 lines
11 KiB
Lua

-- 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