Newer
Older
-- Fetch over HTTPS with peert cert checked
local function https_fetch(url, ca)
local ssl_ok, https = pcall(require, 'ssl.https')
local ltn_ok, ltn12 = pcall(require, 'ltn12')
if not ssl_ok or not ltn_ok then
return nil, 'luasec and luasocket needed for root TA bootstrap'
end
local resp = {}
local r, c, h, s = https.request{
url = url,
cafile = ca,
verify = {'peer', 'fail_if_no_peer_cert' },
protocol = 'tlsv1_2',
sink = ltn12.sink.table(resp),
}
if r == nil then return r, c end
return resp[1]
end
-- Fetch root anchors in XML over HTTPS, returning a zone-file-style string.
local function bootstrap(url, ca)
-- @todo ICANN certificate is verified against current CA
-- this is not ideal, as it should rather verify .xml signature which
-- is signed by ICANN long-lived cert, but luasec has no PKCS7
ca = ca or etcdir..'/icann-ca.pem'
url = url or 'https://data.iana.org/root-anchors/root-anchors.xml'
local xml, err = https_fetch(url, ca)
if not xml then
return false, string.format('[ ta ] fetch of "%s" failed: %s', url, err)
end
local rr = ''
-- Parse root trust anchor, one digest at a time, converting to a zone-file-style string.
string.gsub(xml, "<KeyDigest[^>]*>(.-)</KeyDigest>", function (xml1)
local fields = {}
string.gsub(xml1, "<([%w]+).->([^<]+)</[%w]+>", function (k, v) fields[k] = v end)
rr = rr .. '\n' .. string.format('. 0 IN DS %s %s %s %s',
fields.KeyTag, fields.Algorithm, fields.DigestType, fields.Digest)
end)
-- Add to key set, create an empty keyset file to be filled
print('[ ta ] warning: root anchor bootstrapped, you SHOULD check the key manually, see: '..
'https://data.iana.org/root-anchors/draft-icann-dnssec-trust-anchor.html#sigs')
return rr
end
-- Load the module
local ffi = require 'ffi'
local kres = require('kres')
-- 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)
-- Match key owner and content
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 == C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata) 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
local ds_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
local dnskey_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
if ds_tag == dnskey_tag then
return ta
end
end
end
end
return nil
end
-- Evaluate TA status according to RFC5011
local function ta_present(keyset, rr, hold_down_time, force)
if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then
return false -- Ignore
-- 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 key_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #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 = os.time() + 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 or verbose() then
print('[ ta ] key: '..key_tag..' state: '..ta.state)
end
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
rr.state = key_state.AddPend
rr.timer = now + hold_down_time
end
if rr.state ~= key_state.Valid or verbose() then
print('[ ta ] key: '..key_tag..' state: '..rr.state)
end
table.insert(keyset, rr)
return true
end
return false
end
-- TA is missing in the new key set
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 ta.state == key_state.Valid then
ta.state = key_state.Missing
ta.timer = os.time() + hold_down_time
-- Purge pending key
elseif ta.state == key_state.AddPend then
print('[ ta ] key: '..key_tag..' purging')
keep_ta = false
end
print('[ ta ] key: '..key_tag..' state: '..ta.state)
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, priming, bootstrap)
trust_anchors.refresh_ev = event.after(timeout, function (ev)
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), bootstrap)
if trust_anchors.refresh_time ~= nil then
next_time = trust_anchors.refresh_time
print('[ ta ] next refresh: '..next_time)
refresh_plan(trust_anchors, next_time, refresh_cb)
-- Priming query, prime root NS next
if priming ~= nil then
resolve('.', kres.type.NS, kres.class.IN)
end
end)
end)
end
-- Active refresh, return time of the next check
local function active_refresh(trust_anchors, pkt, bootstrap)
local retry = true
if pkt:rcode() == kres.rcode.NOERROR then
local records = pkt:section(kres.section.ANSWER)
local keyset = {}
if rr.type == kres.type.DNSKEY then
table.insert(keyset, rr)
end
end
trust_anchors.update(keyset, bootstrap)
retry = false
else
print('[ ta ] active refresh failed, rcode: '..pkt:rcode())
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 keep_removed = trust_anchors.keep_removed
for i, ta in ipairs(trust_anchors.keyset) do
local keep = true
if not ta_find(new_keys, ta) then
keep = ta_missing(ta, hold_down)
-- 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
end
end
-- Evaluate new TAs
if (rr.type == kres.type.DNSKEY or rr.type == kres.type.DS) and rr.rdata ~= nil then
ta_present(keyset, rr, hold_down, initial)
end
end
-- Publish active TAs
local store = kres.context().trust_anchors
C.kr_ta_clear(store)
if next(keyset) == nil then return false end
for i, ta in ipairs(keyset) do
-- Key MAY be used as a TA only in these two states (RFC5011, 4.2)
if ta.state == key_state.Valid or ta.state == key_state.Missing then
C.kr_ta_add(store, ta.owner, ta.type, ta.ttl, ta.rdata, #ta.rdata)
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, unmanaged)
-- Bootstrap if requested and keyfile doesn't exist
if trust_anchors.refresh_ev ~= nil then event.cancel(trust_anchors.refresh_ev) end
if not unmanaged then
if not io.open(path, 'r') then
local rr, msg = bootstrap()
if not rr then
error('you MUST obtain the root TA manually, see: '..
'https://knot-resolver.readthedocs.io/en/latest/daemon.html#enabling-dnssec')
end
trustanchor(rr)
-- Fetch DNSKEY immediately
trust_anchors.file_current = path
refresh_plan(trust_anchors, 0, active_refresh, true, true)
return
elseif path == trust_anchors.file_current then
return
end
end
-- Parse new keys, refresh eventually
local new_keys = require('zonefile').file(path)
if unmanaged then
trust_anchors.file_current = nil
else
trust_anchors.file_current = path
end
trust_anchors.keyset = {}
if trust_anchors.update(new_keys, true) then
refresh_plan(trust_anchors, 10 * sec, active_refresh, true, false)
end,
-- Add DS/DNSKEY record(s) (unmanaged)
add = function (keystr)
return trustanchor(keystr)
end,
-- Negative TA management
set_insecure = function (list)
local store = kres.context().negative_anchors
C.kr_ta_clear(store)
for i = 1, #list do
local dname = kres.str2dname(list[i])
C.kr_ta_add(store, dname, kres.type.DS, 0, nil, 0)
end
trust_anchors.insecure = list
end,
}