trust_anchors.lua 9.41 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
-- 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
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
	-- Parse root trust anchor
	local fields = {}
	string.gsub(xml, "<([%w]+).->([^<]+)</[%w]+>", function (k, v) fields[k] = v end)
	local rrdata = string.format('%s %s %s %s', fields.KeyDigest, fields.Algorithm, fields.DigestType, fields.Digest)
	local rr = string.format('%s 0 IN DS %s', fields.TrustAnchor, rrdata)
	-- 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 (check for FFI)
43
44
local ffi_ok, ffi = pcall(require, 'ffi')
if not ffi_ok then
45
46
47
48
49
50
51
52
53
54
	-- Simplified TA management, no RFC5011 automatics
	return {
		-- Reuse Lua/C global function
		add = trustanchor,
		-- Simplified trust anchor management
		config = function (path)
			if not path then return end
			if not io.open(path, 'r') then
				local rr, err = bootstrap()
				if not rr then print(err) return false end
55
56
				local keyfile = assert(io.open(path, 'w'))
				keyfile:write(rr..'\n')
57
58
59
60
61
62
63
64
			end
			for line in io.lines(path) do
				trustanchor(line)
			end
		end,
		-- Disabled
		set_insecure = function () error('[ ta ] FFI not available, this function is disabled') end,
	}
65
end
66
local kres = require('kres')
67
local C = ffi.C
68
69
70
71
72
73
74
75
76

-- 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)
Marek Vavruša's avatar
Marek Vavruša committed
77
	for i, ta in ipairs(keyset) do
78
79
		-- Match key owner and content
		if ta.owner == rr.owner and
80
		   C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) == 0 then
81
82
83
84
85
86
87
		   return ta
		end
	end
	return nil
end

-- Evaluate TA status according to RFC5011
88
local function ta_present(keyset, rr, hold_down_time, force)
89
90
	if not C.kr_dnssec_key_ksk(rr.rdata) then
		return false -- Ignore
91
	end
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
	-- 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
120
		print('[ ta ] key: '..key_tag..' state: '..ta.state)
121
122
		return true
	elseif not key_revoked then -- First time seen (NewKey)
123
		rr.key_tag = key_tag
124
125
126
127
128
129
		if force then
			rr.state = key_state.Valid
		else
			rr.state = key_state.AddPend
			rr.timer = now + hold_down_time
		end
130
		print('[ ta ] key: '..key_tag..' state: '..rr.state)
131
132
133
134
135
136
137
		table.insert(keyset, rr)
		return true
	end
	return false
end

-- TA is missing in the new key set
138
local function ta_missing(keyset, ta, hold_down_time)
139
140
141
142
143
144
145
146
	-- 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
147
		print('[ ta ] key: '..key_tag..' purging')
148
149
		keep_ta = false
	end
150
	print('[ ta ] key: '..key_tag..' state: '..ta.state)
151
152
153
	return keep_ta
end

154
-- Plan refresh event and re-schedule itself based on the result of the callback
155
local function refresh_plan(trust_anchors, timeout, refresh_cb, priming, bootstrap)
156
	trust_anchors.refresh_ev = event.after(timeout, function (ev)
157
		resolve('.', kres.type.DNSKEY, kres.class.IN, kres.query.NO_CACHE,
158
159
		function (pkt)
			-- Schedule itself with updated timeout
160
			local next_time = refresh_cb(trust_anchors, kres.pkt_t(pkt), bootstrap)
161
162
163
			if trust_anchors.refresh_time ~= nil then
				next_time = math.min(next_time, trust_anchors.refresh_time)
			end
164
			print('[ ta ] next refresh: '..next_time)
165
			refresh_plan(trust_anchors, next_time, refresh_cb)
166
167
168
169
			-- Priming query, prime root NS next
			if priming ~= nil then
				resolve('.', kres.type.NS, kres.class.IN)
			end
170
171
172
173
174
		end)
	end)
end

-- Active refresh, return time of the next check
175
local function active_refresh(trust_anchors, pkt, bootstrap)
176
177
178
179
	local retry = true
	if pkt:rcode() == kres.rcode.NOERROR then
		local records = pkt:section(kres.section.ANSWER)
		local keyset = {}
Marek Vavruša's avatar
Marek Vavruša committed
180
		for i, rr in ipairs(records) do
181
182
183
184
			if rr.type == kres.type.DNSKEY then
				table.insert(keyset, rr)
			end
		end
185
		trust_anchors.update(keyset, bootstrap)
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
		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

211
212
213
214
-- TA store management
local trust_anchors = {
	keyset = {},
	insecure = {},
215
	hold_down_time = 30 * day,
216
217
218
219
	-- 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)
220
		local hold_down = trust_anchors.hold_down_time / 1000
Marek Vavruša's avatar
Marek Vavruša committed
221
222
		local keyset = {}
		for i, ta in ipairs(trust_anchors.keyset) do
223
224
			local keep = true
			if not ta_find(new_keys, ta) then
Marek Vavruša's avatar
Marek Vavruša committed
225
				keep = ta_missing(trust_anchors, trust_anchors.keyset, ta, hold_down)
226
227
			end
			if keep then
Marek Vavruša's avatar
Marek Vavruša committed
228
				table.insert(keyset, ta)
229
230
231
			end
		end
		-- Evaluate new TAs
Marek Vavruša's avatar
Marek Vavruša committed
232
233
		for i, rr in ipairs(new_keys) do
			if rr.type == kres.type.DNSKEY and rr.rdata ~= nil then
234
				ta_present(keyset, rr, hold_down, initial)
235
236
237
238
239
			end
		end
		-- Publish active TAs
		local store = kres.context().trust_anchors
		C.kr_ta_clear(store)
Marek Vavruša's avatar
Marek Vavruša committed
240
241
		if next(keyset) == nil then return false end
		for i, ta in ipairs(keyset) do
242
243
244
245
246
247
			-- 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
248
249
250
251
		-- Store keyset in the file
		if trust_anchors.file_current ~= nil then
			keyset_write(keyset, trust_anchors.file_current)
		end
252
253
254
		return true
	end,
	-- Load keys from a file (managed)
255
	config = function (path, unmanaged)
256
		-- Bootstrap if requested and keyfile doesn't exist
257
258
259
		if not io.open(path, 'r') then
			local rr, msg = bootstrap()
			if not rr then
260
261
262
				error('you MUST obtain the root TA manually, see: '..
				      'http://knot-resolver.readthedocs.org/en/latest/daemon.html#enabling-dnssec')
			end
263
			trustanchor(rr)
264
265
266
267
268
			-- Fetch DNSKEY immediately
			trust_anchors.file_current = path
			if trust_anchors.refresh_ev ~= nil then event.cancel(trust_anchors.refresh_ev) end
			refresh_plan(trust_anchors, 0, active_refresh, true, true)
			return
269
270
271
		elseif path == trust_anchors.file_current then
			return
		end
272
		-- Parse new keys, refresh eventually
273
		local new_keys = require('zonefile').file(path)
274
		trust_anchors.file_current = path
275
		if unmanaged then trust_anchors.file_current = nil end
276
		trust_anchors.keyset = {}
277
		if trust_anchors.update(new_keys, true) then
278
			if trust_anchors.refresh_ev ~= nil then event.cancel(trust_anchors.refresh_ev) end
279
			refresh_plan(trust_anchors, 5 * sec, active_refresh, true, false)
280
		end
281
282
283
	end,
	-- Add DS/DNSKEY record(s) (unmanaged)
	add = function (keystr)
284
		return trustanchor(keystr)
285
286
287
288
289
290
291
292
293
294
295
296
297
298
	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