trust_anchors.lua 6.84 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
local kres = require('kres')
local C = require('ffi').C

-- 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)
	for i = 1, #keyset do
		local ta = keyset[i]
		-- Match key owner and content
		if ta.owner == rr.owner and
16
		   C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) == 0 then
17 18 19 20 21 22 23
		   return ta
		end
	end
	return nil
end

-- Evaluate TA status according to RFC5011
24
local function ta_present(keyset, rr, hold_down_time, force)
25 26
	if not C.kr_dnssec_key_ksk(rr.rdata) then
		return false -- Ignore
27
	end
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
	-- 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)
	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
		print('[trust_anchors] key: '..key_tag..' state: '..ta.state)
		return true
	elseif not key_revoked then -- First time seen (NewKey)
59
		rr.key_tag = key_tag
60 61 62 63 64 65 66 67 68 69 70 71 72 73
		if force then
			rr.state = key_state.Valid
		else
			rr.state = key_state.AddPend
			rr.timer = now + hold_down_time
		end
		print('[trust_anchors] key: '..key_tag..' state: '..rr.state)
		table.insert(keyset, rr)
		return true
	end
	return false
end

-- TA is missing in the new key set
74
local function ta_missing(keyset, ta, hold_down_time)
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
	-- 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('[trust_anchors] key: '..key_tag..' purging')
		keep_ta = false
	end
	print('[trust_anchors] key: '..key_tag..' state: '..ta.state)
	return keep_ta
end

90
-- Plan refresh event and re-schedule itself based on the result of the callback
91
local function refresh_plan(trust_anchors, timeout, refresh_cb, priming)
92
	trust_anchors.refresh_ev = event.after(timeout, function (ev)
93
		resolve('.', kres.type.DNSKEY, kres.class.IN, kres.query.NO_CACHE,
94 95 96
		function (pkt)
			-- Schedule itself with updated timeout
			local next_time = refresh_cb(trust_anchors, kres.pkt_t(pkt))
97 98 99
			if trust_anchors.refresh_time ~= nil then
				next_time = math.min(next_time, trust_anchors.refresh_time)
			end
100 101
			print('[trust_anchors] next refresh: '..next_time)
			refresh_plan(trust_anchors, next_time, refresh_cb)
102 103 104 105
			-- Priming query, prime root NS next
			if priming ~= nil then
				resolve('.', kres.type.NS, kres.class.IN)
			end
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
		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

148 149 150 151
-- TA store management
local trust_anchors = {
	keyset = {},
	insecure = {},
152
	hold_down_time = 30 * day,
153 154 155 156
	-- 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)
157
		local hold_down = trust_anchors.hold_down_time / 1000
158 159 160 161 162 163
		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
164
				keep = ta_missing(trust_anchors, keyset, ta, hold_down)
165 166
			end
			if keep then
167
				table.insert(keyset_keep, ta)
168 169 170 171 172 173 174
			end
		end
		keyset = keyset_keep
		-- Evaluate new TAs
		for i = 1, #new_keys do
			local rr = new_keys[i]
			if rr.type == kres.type.DNSKEY then
175
				ta_present(keyset, rr, hold_down, initial)
176 177 178 179 180
			end
		end
		-- Publish active TAs
		local store = kres.context().trust_anchors
		C.kr_ta_clear(store)
181
		if #keyset == 0 then return false end
182 183 184 185 186 187 188 189
		for i = 1, #keyset do
			local ta = keyset[i]
			-- 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
190 191 192 193
		-- Store keyset in the file
		if trust_anchors.file_current ~= nil then
			keyset_write(keyset, trust_anchors.file_current)
		end
194 195 196
		return true
	end,
	-- Load keys from a file (managed)
197
	config = function (path, is_unmanaged)
198
		if path == trust_anchors.file_current then return end
199
		local new_keys = require('zonefile').parse_file(path)
200 201 202 203
		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
204
			if trust_anchors.refresh_ev ~= nil then event.cancel(trust_anchors.refresh_ev) end
205
			refresh_plan(trust_anchors, sec, active_refresh, true)
206
		end
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
	end,
	-- Add DS/DNSKEY record(s) (unmanaged)
	add = function (keystr)
		local store = kres.context().trust_anchors
		require('zonefile').parser(function (p)
			local rr = p:current_rr()
			C.kr_ta_add(store, rr.owner, rr.type, rr.ttl, rr.rdata, #rr.rdata)
		end):read(keystr..'\n')
	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,
}

return trust_anchors