trust_anchors.lua.in 19.3 KB
Newer Older
1 2 3 4 5
-- Load the module
local ffi = require 'ffi'
local kres = require('kres')
local C = ffi.C

6 7
local trust_anchors -- the public pseudo-module, exported as global variable

8 9 10 11 12 13 14 15
-- 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 = {}
16
	local r, c = https.request{
17 18 19 20 21 22 23 24 25 26
	       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

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
-- remove UTC timezone specification if present or throw error
local function time2utc(orig_timespec)
	local patterns = {'[+-]00:00$', 'Z$'}
	for _, pattern in ipairs(patterns) do
		local timespec, removals = string.gsub(orig_timespec, pattern, '')
		if removals == 1 then
			return timespec
		end
	end
	error(string.format('unsupported time specification: %s', orig_timespec))
end

local function keydigest_is_valid(valid_from, valid_until)
	local format =            '%Y-%m-%dT%H:%M:%S'
	local time_now = os.date('!%Y-%m-%dT%H:%M:%S')  -- ! forces UTC
	local time_diff = ffi.new('double[1]')
	local err = ffi.C.kr_strptime_diff(
		format, time_now, time2utc(valid_from), time_diff)
	if (err ~= nil) then
	       error(string.format('failed to process "validFrom" constraint: %s',
				   ffi.string(err)))
	end
	local from_ok = time_diff[0] > 0

	-- optional attribute
	local until_ok = true
	if valid_until then
		err = ffi.C.kr_strptime_diff(
			format, time_now, time2utc(valid_until), time_diff)
		if (err ~= nil) then
			error(string.format('failed to process "validUntil" constraint: %s',
					    ffi.string(err)))
		end
		until_ok = time_diff[0] < 0
	end
	return from_ok and until_ok
end

local function parse_xml_keydigest(attrs, inside, output)
	local fields = {}
	local _, n = string.gsub(attrs, "([%w]+)=\"([^\"]*)\"", function (k, v) fields[k] = v end)
	assert(n >= 1,
		string.format('cannot parse XML attributes from "%s"', attrs))
	assert(fields['validFrom'],
		string.format('mandatory KeyDigest XML attribute validFrom ' ..
		'not found in "%s"', attrs))
73 74 75 76 77 78
	local valid_attrs = {id = true, validFrom = true, validUntil = true}
	for key, _ in pairs(fields) do
		assert(valid_attrs[key],
		       string.format('unsupported KeyDigest attribute "%s" found in "%s"',
				     key, attrs))
	end
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

	_, n = string.gsub(inside, "<([%w]+).->([^<]+)</[%w]+>", function (k, v) fields[k] = v end)
	assert(n >= 1,
		string.format('error parsing KeyDigest XML elements from "%s"',
			      inside))
	local mandatory_elements = {'KeyTag', 'Algorithm', 'DigestType', 'Digest'}
	for _, key in ipairs(mandatory_elements) do
		assert(fields[key],
			string.format('mandatory element %s is missing in "%s"',
				      key, inside))
	end
	assert(n == 4, string.format('found %d elements but expected 4 in %s', n, inside))
	table.insert(output, fields)  -- append to list of parsed keydigests
end

local function generate_ds(keydigests)
	local rrset = ''
	for _, fields in ipairs(keydigests) do
		local rr = string.format(
			'. 0 IN DS %s %s %s %s',
			fields.KeyTag, fields.Algorithm, fields.DigestType, fields.Digest)
		if keydigest_is_valid(fields['validFrom'], fields['validUntil']) then
			rrset = rrset .. '\n' .. rr
		else
			log('[ ta ] skipping trust anchor "%s" ' ..
			    'because it is outside of validity range', rr)
		end
	end
	return rrset
end

local function assert_str_match(str, pattern, expected)
	local count = 0
	for _ in string.gmatch(str, pattern) do
		count = count + 1
	end
	assert(count == expected,
	       string.format('expected %d occurences of "%s" but got %d in "%s"',
			     expected, pattern, count, str))
end

120 121
-- Fetch root anchors in XML over HTTPS, returning a zone-file-style string
-- or false in case of error, and a message.
122
local function bootstrap(url, ca)
Vladimír Čunát's avatar
Vladimír Čunát committed
123
	-- RFC 7958, sec. 2, but we don't do precise XML parsing.
124 125 126 127 128 129 130
	-- @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
	local xml, err = https_fetch(url, ca)
	if not xml then
		return false, string.format('[ ta ] fetch of "%s" failed: %s', url, err)
	end
131 132 133 134 135 136 137

	-- we support only minimal subset of https://tools.ietf.org/html/rfc7958
	assert_str_match(xml, '<?xml version="1%.0" encoding="UTF%-8"%?>', 1)
	assert_str_match(xml, '<TrustAnchor ', 1)
	assert_str_match(xml, '<Zone>.</Zone>', 1)
	assert_str_match(xml, '</TrustAnchor>', 1)

138
	-- Parse root trust anchor, one digest at a time, converting to a zone-file-style string.
139 140 141
	local keydigests = {}
	string.gsub(xml, "<KeyDigest([^>]*)>(.-)</KeyDigest>", function(attrs, inside)
		parse_xml_keydigest(attrs, inside, keydigests)
142
	end)
143 144 145
	local rrset = generate_ds(keydigests)
	if rrset == '' then
		return false, string.format('[ ta ] no valid trust anchors found at "%s"', url)
146 147
	end
	local msg = '[ ta ] Root trust anchors bootstrapped over https with pinned certificate.\n'
148 149
			 .. '       You SHOULD verify them manually against original source:\n'
			 .. '       https://www.iana.org/dnssec/files\n'
150
			 .. '[ ta ] Current root trust anchors are:'
151 152
			 .. rrset
	return rrset, msg
153 154
end

155 156 157 158 159 160 161 162
-- 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)
163 164 165
	local rr_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
	assert(rr_tag >= 0 and rr_tag <= 65535, string.format('invalid RR: %s: %s',
	       kres.rr2str(rr), ffi.string(C.knot_strerror(rr_tag))))
Marek Vavruša's avatar
Marek Vavruša committed
166
	for i, ta in ipairs(keyset) do
167
		-- Match key owner and content
168 169 170
		local ta_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
		assert(ta_tag >= 0 and ta_tag <= 65535, string.format('invalid RR: %s: %s',
		       kres.rr2str(ta), ffi.string(C.knot_strerror(ta_tag))))
171 172 173 174 175 176 177 178 179 180 181
		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
182
				if ta.key_tag == rr_tag then
183 184 185 186 187 188 189
					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
190
				if rr_tag == ta_tag then
191
					return ta
Vladimír Čunát's avatar
Vladimír Čunát committed
192
				end
193
			end
194 195 196 197 198
		end
	end
	return nil
end

199
-- Evaluate TA status of a RR according to RFC5011.  The time is in seconds.
200
local function ta_present(keyset, rr, hold_down_time, force_valid)
201
	if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then
202
		return false -- Ignore
203
	end
204 205
	-- Find the key in current key set and check its status
	local now = os.time()
206
	local key_revoked = (rr.type == kres.type.DNSKEY) and C.kr_dnssec_key_revoked(rr.rdata)
207
	local key_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
208 209
	assert(key_tag >= 0 and key_tag <= 65535, string.format('invalid RR: %s: %s',
	       kres.rr2str(rr), ffi.string(C.knot_strerror(key_tag))))
210 211 212 213 214 215 216 217 218 219 220
	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
Vladimír Čunát's avatar
Vladimír Čunát committed
221
				ta.timer = now + hold_down_time
222 223 224 225 226 227 228 229 230 231 232 233
			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
234
		if rr.state ~= key_state.Valid or verbose() then
235
			log('[ ta ] key: ' .. key_tag .. ' state: '..ta.state)
236
		end
237 238
		return true
	elseif not key_revoked then -- First time seen (NewKey)
239
		rr.key_tag = key_tag
240
		if force_valid then
241 242 243 244 245
			rr.state = key_state.Valid
		else
			rr.state = key_state.AddPend
			rr.timer = now + hold_down_time
		end
246
		if rr.state ~= key_state.Valid or verbose() then
247
			log('[ ta ] key: ' .. key_tag .. ' state: '..rr.state)
248
		end
249 250 251 252 253 254
		table.insert(keyset, rr)
		return true
	end
	return false
end

255
-- TA is missing in the new key set.  The time is in seconds.
256
local function ta_missing(ta, hold_down_time)
257 258 259
	-- Key is removed (KeyRem)
	local keep_ta = true
	local key_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
260 261
	assert(key_tag >= 0 and key_tag <= 65535, string.format('invalid RR: %s: %s',
	       kres.rr2str(ta), ffi.string(C.knot_strerror(key_tag))))
262 263 264
	if ta.state == key_state.Valid then
		ta.state = key_state.Missing
		ta.timer = os.time() + hold_down_time
265 266 267 268

	-- 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
269
		log('[ ta ] key: '..key_tag..' removed because missing for too long')
270 271
		keep_ta = false

272 273
	-- Purge pending key
	elseif ta.state == key_state.AddPend then
274
		log('[ ta ] key: '..key_tag..' purging')
275 276
		keep_ta = false
	end
277
	log('[ ta ] key: '..key_tag..' state: '..ta.state)
278 279 280
	return keep_ta
end

281 282
local active_refresh, update -- forwards

Vladimír Čunát's avatar
Vladimír Čunát committed
283
-- Plan an event for refreshing the root DNSKEYs and re-scheduling itself
284
local function refresh_plan(keyset, delay, is_initial)
285
	local owner_str = kres.dname2str(keyset.owner) -- maybe fix converting back and forth?
286
	keyset.refresh_ev = event.after(delay, function ()
287
		resolve(owner_str, kres.type.DNSKEY, kres.class.IN, 'NO_CACHE',
288 289
		function (pkt)
			-- Schedule itself with updated timeout
290 291 292 293 294
			local delay_new = active_refresh(keyset, kres.pkt_t(pkt), is_initial)
			delay_new = keyset.refresh_time or trust_anchors.refresh_time or delay_new
			log('[ ta ] next refresh for ' .. owner_str .. ' in '
					.. delay_new/hour .. ' hours')
			refresh_plan(keyset, delay_new)
295 296 297 298
		end)
	end)
end

299 300
-- Refresh the DNSKEYs from the packet, and return time to the next check.
active_refresh = function (keyset, pkt, is_initial)
301 302 303
	local retry = true
	if pkt:rcode() == kres.rcode.NOERROR then
		local records = pkt:section(kres.section.ANSWER)
304
		local new_keys = {}
305
		for _, rr in ipairs(records) do
306
			if rr.type == kres.type.DNSKEY then
307
				table.insert(new_keys, rr)
308 309
			end
		end
310
		update(keyset, new_keys, is_initial)
311
		retry = false
312
	else
313 314
		warn('[ ta ] active refresh failed for ' .. kres.dname2str(keyset.owner)
				.. ' with rcode: ' .. pkt:rcode())
315 316 317
	end
	-- Calculate refresh/retry timer (RFC 5011, 2.3)
	local min_ttl = retry and day or 15 * day
318
	for _, rr in ipairs(keyset) do -- 10 or 50% of the original TTL
319 320 321 322 323
		min_ttl = math.min(min_ttl, (retry and 100 or 500) * rr.ttl)
	end
	return math.max(hour, min_ttl)
end

324
-- Write keyset to a file.  States and timers are stored in comments.
325 326
local function keyset_write(keyset)
	if not keyset.filename then return false end -- not to be persisted
327 328
	local fname_tmp = keyset.filename .. '.lock.' .. tostring(worker.pid);
	local file = assert(io.open(fname_tmp, 'w'))
329 330
	for i = 1, #keyset do
		local ta = keyset[i]
331
		ta.comment = ' ' .. ta.state .. ':' .. (ta.timer or '')
332
					.. ' ; KeyTag:' .. ta.key_tag -- the tag is just for humans
333
		local rr_str = kres.rr2str(ta) .. '\n'
334
		if ta.state ~= key_state.Valid and ta.state ~= key_state.Missing then
335
			rr_str = '; '..rr_str -- Invalidate key string (for older kresd versions)
336 337 338 339
		end
		file:write(rr_str)
	end
	file:close()
340
	assert(os.rename(fname_tmp, keyset.filename))
341 342 343 344 345 346 347 348 349 350 351 352 353 354
end

-- Search the values of a table and return the corrseponding key (or nil).
local function table_search(t, val)
	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)
355
	for _, ta in pairs(tas) do
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
		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')
374 375 376 377
	local tas, err = zonefile.file(path)
	if not tas then
		return tas, err
	end
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
	keyset_parse_comments(tas, key_state.Valid)

	-- The untrusted keys are commented out but important to load.
	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
393

394
	for _, ta in pairs(tas) do
395 396 397 398 399 400
		local ta_keytag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
		if not (ta_keytag >= 0 and ta_keytag <= 65535) then
			return nil, string.format('invalid key: "%s": %s',
				kres.rr2str(ta), ffi.string(C.knot_strerror(ta_keytag)))
		end
		ta.key_tag = ta_keytag
401
	end
402 403 404
	return tas
end

405 406
-- Replace current TAs for given owner by the "trusted" ones from passed keyset.
-- Return the number of trusted keys for the owner.
407 408 409
local function keyset_publish(keyset)
	local store = kres.context().trust_anchors
	local count = 0
410
	C.kr_ta_del(store, keyset.owner)
411
	for _, ta in ipairs(keyset) do
412 413 414 415 416 417 418
		-- 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
419 420 421 422
	if count == 0 then
		warn('[ ta ] ERROR: no anchors are trusted for ' ..
		     kres.dname2str(keyset.owner) .. ' !')
	end
423
	return count
424 425
end

Vladimír Čunát's avatar
Vladimír Čunát committed
426

427 428
-- Update existing keyset; return true if successful.
-- Param `is_initial` (bool): force .NewKey states to .Valid, i.e. init empty keyset.
429
update = function (keyset, new_keys, is_initial)
430
	if not new_keys then return false end
431 432 433 434 435 436

	-- 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 trust_anchors.hold_down_time) / 1000
	local keepset = {}
	local keep_removed = keyset.keep_removed or trust_anchors.keep_removed
437
	for _, ta in ipairs(keyset) do
438 439 440 441 442
		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)
443
		end
444 445 446 447 448 449
		-- Purge removed keys
		if ta.state == key_state.Removed then
			if keep_removed > 0 then
				keep_removed = keep_removed - 1
			else
				keep = false
450 451
			end
		end
452
		if keep then
453
			table.insert(keepset, ta)
454
		end
455
	end
456
	-- 2: remove all TAs - other settings etc. will remain in the keyset
457
	for i, _ in ipairs(keyset) do
458 459 460 461 462 463 464
		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

465
	-- Evaluate new TAs
466
	for _, rr in ipairs(new_keys) do
467 468
		if (rr.type == kres.type.DNSKEY or rr.type == kres.type.DS) and rr.rdata ~= nil then
			ta_present(keyset, rr, hold_down, is_initial)
469
		end
470
	end
471

472 473
	-- Store the keyset
	keyset_write(keyset)
474

475 476
	-- Start using the new TAs.
	if keyset_publish(keyset) == 0 then
477
		-- TODO: try to rebootstrap if for root?
478 479 480 481 482 483
		return false
	end

	return true
end

484
local add_file = function (path, unmanaged)
485 486 487 488 489 490
	if not unmanaged then
		if not io.open(path .. '.lock', 'w') then
			error("[ ta ] ERROR: write access needed to keyfile dir '"..path.."'")
		end
		os.remove(path .. ".lock")
	end
491

492
	-- Bootstrap if requested and keyfile doesn't exist
493
	if not unmanaged and not io.open(path, 'r') then
494
		log("[ ta ] keyfile '%s': doesn't exist, bootstrapping", path);
495 496 497 498 499 500 501 502 503 504 505 506
		local tas, msg = bootstrap(trust_anchors.bootstrap_url, trust_anchors.bootstrap_ca)
		if not tas then
			msg = msg .. '\n'
				.. '[ ta ] Failed to bootstrap root trust anchors; see:\n'
				.. '       https://knot-resolver.readthedocs.io/en/latest/daemon.html#enabling-dnssec'
			error(msg)
		end
		print(msg)
		trustanchor(tas)
		-- Fetch DNSKEY immediately
		if not trust_anchors.keysets['\0'] then
			trust_anchors.keysets['\0'] = { owner = '\0' }
507
		end
508 509 510
		local keyset = trust_anchors.keysets['\0']
		keyset.filename = path
		if keyset.refresh_ev then event.cancel(keyset.refresh_ev) end
511
		refresh_plan(keyset, 0, true)
512
		return
513
	end
514 515 516 517 518
	if not unmanaged and path == (trust_anchors.keysets['\0'] or {}).filename then
		return
	end

	-- Parse the file and check its sanity
519
	local keyset, err = keyset_read(path)
520 521 522
	if not keyset then
		panic("[ ta ] ERROR: failed to read anchors from '%s' (%s)", path, err)
	end
523
	if not unmanaged then keyset.filename = path end
524
	if not keyset[1] then
525
		panic("[ ta ] ERROR: failed to read anchors from '%s'", path)
526
	end
527
	if not unmanaged then keyset.filename = path end
528
	local owner = keyset[1].owner
529
	for _, ta in ipairs(keyset) do
530
		if ta.owner ~= owner then
531
			panic("[ ta ] ERROR: mixed owner names found in '%s'", path)
532
		end
533
	end
534 535 536 537 538 539 540 541 542 543
	keyset.owner = owner

	local owner_str = kres.dname2str(owner)
	if trust_anchors.keysets[owner] then
		warn('[ ta ] warning: overriding previously set trust anchors for ' .. owner_str)
		local refresh_ev = trust_anchors.keysets[owner].refresh_ev
		if refresh_ev then event.cancel(refresh_ev) end
	end
	trust_anchors.keysets[owner] = keyset

544
	-- Parse new keys, refresh eventually
545
	if keyset_publish(keyset) == 0 then
546
		-- TODO: try to rebootstrap if for root?
547
	end
548
	refresh_plan(keyset, 10 * sec, false)
549 550 551
end


552
-- TA store management, for user docs see ../README.rst
553
trust_anchors = {
554 555 556 557 558 559 560 561 562
	-- [internal] table indexed by dname;
	--  each item is a list of RRs and additionally contains:
	--   - owner - that dname (for simplicity)
	--   - [optional] filename in which to persist the state
	--   - [optional] overrides for global defaults of
	--     hold_down_time, refresh_time, keep_removed
	-- The RR tables also contain some additional TA-specific fields.
	keysets = {},

563 564 565 566 567 568 569 570
	-- Documented properties:
	insecure = {},
	hold_down_time = 30 * day,
	refresh_time = nil,
	keep_removed = 0,

	bootstrap_url = 'https://data.iana.org/root-anchors/root-anchors.xml',
	bootstrap_ca = '@ETCDIR@/icann-ca.pem',
571 572
	-- change empty string to nil
	keyfile_default = ('@KEYFILE_DEFAULT@' ~= '' and '@KEYFILE_DEFAULT@') or nil,
573

574 575 576 577
	-- Load keys from a file, 5011-managed by default.
	-- If managed and the file doesn't exist, try bootstrapping the root into it.
	add_file = add_file,
	config = add_file,
578

579 580
	-- Add DS/DNSKEY record(s) (unmanaged)
	add = function (keystr)
581
		return trustanchor(keystr)
582 583 584 585 586 587 588 589 590 591 592 593 594
	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,
}

Vladimír Čunát's avatar
Vladimír Čunát committed
595 596 597 598 599 600 601 602 603
-- Syntactic sugar for TA store
setmetatable(trust_anchors, {
	__newindex = function (t,k,v)
	if     k == 'file' then t.config(v)
	elseif k == 'negative' then t.set_insecure(v)
	else   rawset(t, k, v) end
	end,
})

604
return trust_anchors