]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
trust anchors: persist the state and timer
authorVladimír Čunát <vladimir.cunat@nic.cz>
Tue, 14 Feb 2017 15:44:27 +0000 (16:44 +0100)
committerVladimír Čunát <vladimir.cunat@nic.cz>
Mon, 27 Feb 2017 10:15:01 +0000 (11:15 +0100)
The format of TA store is compatible both ways with old kresd.
Note: it requires the parent commit to work, i.e. new libzscanner.

daemon/lua/trust_anchors.lua.in

index d4afdfab61cbe5f7f391d57804839c0d9340a634..6c8e0a3b59a0c2c55f667d53144383bbce045d6d 100644 (file)
@@ -91,7 +91,7 @@ local function ta_find(keyset, rr)
 end
 
 -- Evaluate TA status of a RR according to RFC5011.  The time is in seconds.
-local function ta_present(keyset, rr, hold_down_time, force)
+local function ta_present(keyset, rr, hold_down_time, force_valid)
        if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then
                return false -- Ignore
        end
@@ -129,7 +129,7 @@ local function ta_present(keyset, rr, hold_down_time, force)
                return true
        elseif not key_revoked then -- First time seen (NewKey)
                rr.key_tag = key_tag
-               if force then
+               if force_valid then
                        rr.state = key_state.Valid
                else
                        rr.state = key_state.AddPend
@@ -170,12 +170,12 @@ end
 
 local active_refresh -- forward
 -- Plan an event for refreshing the root DNSKEYs and re-scheduling itself
-local function refresh_plan(trust_anchors, timeout, priming, bootstrap)
+local function refresh_plan(trust_anchors, timeout, priming, is_initial)
        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 = active_refresh(trust_anchors, kres.pkt_t(pkt), bootstrap)
+                       local next_time = active_refresh(trust_anchors, kres.pkt_t(pkt), is_initial)
                        if trust_anchors.refresh_time ~= nil then
                                next_time = trust_anchors.refresh_time
                        end
@@ -190,7 +190,7 @@ local function refresh_plan(trust_anchors, timeout, priming, bootstrap)
 end
 
 -- Refresh the root DNSKEYs from the packet, and return time to the next check.
-active_refresh = function (trust_anchors, pkt, bootstrap)
+active_refresh = function (trust_anchors, pkt, is_initial)
        local retry = true
        if pkt:rcode() == kres.rcode.NOERROR then
                local records = pkt:section(kres.section.ANSWER)
@@ -200,7 +200,7 @@ active_refresh = function (trust_anchors, pkt, bootstrap)
                                table.insert(keyset, rr)
                        end
                end
-               trust_anchors.update(keyset, bootstrap)
+               trust_anchors.update(keyset, is_initial)
                retry = false
        else
                print('[ ta ] active refresh failed, rcode: '..pkt:rcode())
@@ -213,19 +213,92 @@ active_refresh = function (trust_anchors, pkt, bootstrap)
        return math.max(hour, min_ttl)
 end
 
--- Write keyset to a file
+-- Write keyset to a file.  States and timers are stored in comments.
 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)
+               ta.comment = ' ' .. ta.state .. ':' .. (ta.timer or '')
+               local rr_str = kres.rr2str(ta) .. '\n'
                if ta.state ~= key_state.Valid and ta.state ~= key_state.Missing then
-                       rr_str = '; '..rr_str -- Invalidate key string
+                       rr_str = '; '..rr_str -- Invalidate key string (for older kresd versions)
                end
                file:write(rr_str)
        end
        file:close()
        os.rename(path..'.lock', path)
+       -- TODO: IO error handling
+end
+
+-- Search the values of a table and return the corrseponding key (or nil).
+local function table_search(t, val)
+       local k, v
+       for k, v in pairs(t) do
+               if v == val then
+                       return k
+               end
+       end
+       return nil
+end
+
+-- For each RR, parse .state and .timer from .comment.
+local function keyset_parse_comments(tas, default_state)
+       local _k, ta
+       for _k, ta in pairs(tas) do
+               ta.state = default_state
+               if ta.comment then
+                       string.gsub(ta.comment, '^%s*(%a+):(%d*)', function (state, time)
+                               if table_search(key_state, state) then
+                                       ta.state = state
+                               end
+                               ta.timer = tonumber(time) -- nil on failure
+                       end)
+                       ta.comment = nil
+               end
+       end
+       return tas
+end
+
+-- Read keyset from a file.  (This includes the key states and timers.)
+local function keyset_read(path)
+       -- First load the regular entries, trusting them.
+       local zonefile = require('zonefile')
+       local tas = zonefile.file(path)
+       keyset_parse_comments(tas, key_state.Valid)
+
+       -- The untrusted keys are commented out but important to load.
+       local line
+       for line in io.lines(path) do
+               if line:sub(1, 2) == '; ' then
+                       -- Ignore the line if it fails to parse including recognized .state.
+                       local l_set = zonefile.string(line:sub(3))
+                       if l_set and l_set[1] then
+                               keyset_parse_comments(l_set)
+                               if l_set[1].state then
+                                       table.insert(tas, l_set[1])
+                               end
+                       end
+               end
+       end
+       return tas
+end
+
+-- Replace current TAs by the "trusted" ones from passed keyset.
+-- Return the number of trusted keys.
+local function keyset_publish(keyset)
+       local store = kres.context().trust_anchors
+       local count = 0
+       C.kr_ta_clear(store)
+       if not next(keyset) then return 0 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
+                       if C.kr_ta_add(store, ta.owner, ta.type, ta.ttl, ta.rdata, #ta.rdata) == 0 then
+                               count = count + 1
+                       end
+               end
+       end
+       return count
 end
 
 -- TA store management
@@ -238,8 +311,8 @@ local trust_anchors = {
        bootstrap_ca = '@ETCDIR@/icann-ca.pem',
 
        -- Update existing keyset; return true if successful.
-       -- Param `initial` (bool): force .NewKey states to .Valid, i.e. init empty keyset.
-       update = function (new_keys, initial)
+       -- Param `is_initial` (bool): force .NewKey states to .Valid, i.e. init empty keyset.
+       update = function (new_keys, is_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
@@ -267,26 +340,26 @@ local trust_anchors = {
                -- Evaluate new TAs
                for i, 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, initial)
+                               ta_present(keyset, rr, hold_down, is_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
+
+               -- Start using the new TAs.
+               trust_anchors.keyset = keyset
+               if keyset_publish(keyset) == 0 then
+                       warn('[ ta ] ERROR: no root anchors are trusted!')
+                       -- TODO: try to rebootstrap?
+                       return false
+               end
+
                return true
        end,
+
        -- Load keys from a file (managed)
        config = function (path, unmanaged)
                -- Bootstrap if requested and keyfile doesn't exist
@@ -310,18 +383,20 @@ local trust_anchors = {
                                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, true, false)
+               -- Parse new keys, refresh eventually
+               trust_anchors.keyset = keyset_read(path)
+               if keyset_publish(trust_anchors.keyset) == 0 then
+                       warn('[ ta ] ERROR: no root anchors are trusted!')
+                       -- TODO: try to rebootstrap?
                end
+               refresh_plan(trust_anchors, 10 * sec, true, false)
        end,
+
        -- Add DS/DNSKEY record(s) (unmanaged)
        add = function (keystr)
                return trustanchor(keystr)