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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
|
-- 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
|