Commit ae2bebbf authored by Vladimír Čunát's avatar Vladimír Čunát

Merge !748: TA bootstrap: support validUntil attribute

Closes #435.
parents 79cbb321 006926c2
Pipeline #44682 failed with stages
in 8 minutes and 56 seconds
......@@ -315,6 +315,7 @@ int kr_zonecut_add(struct kr_zonecut *, const knot_dname_t *, const void *, int)
_Bool kr_zonecut_is_empty(struct kr_zonecut *);
void kr_zonecut_set(struct kr_zonecut *, const knot_dname_t *);
uint64_t kr_now();
const char *kr_strptime_diff(const char *, const char *, const char *, double *);
void lru_free_items_impl(struct lru *);
struct lru *lru_create_impl(unsigned int, knot_mm_t *, knot_mm_t *);
void *lru_get_impl(struct lru *, const char *, unsigned int, unsigned int, _Bool, _Bool *);
......
......@@ -169,6 +169,7 @@ EOF
kr_zonecut_is_empty
kr_zonecut_set
kr_now
kr_strptime_diff
lru_free_items_impl
lru_create_impl
lru_get_impl
......
-- Load the module
local ffi = require 'ffi'
local kres = require('kres')
local C = ffi.C
local trust_anchors -- the public pseudo-module, exported as global variable
-- Fetch over HTTPS with peert cert checked
......@@ -19,6 +24,99 @@ local function https_fetch(url, ca)
return resp[1]
end
-- 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))
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
_, 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
-- Fetch root anchors in XML over HTTPS, returning a zone-file-style string
-- or false in case of error, and a message.
local function bootstrap(url, ca)
......@@ -30,30 +128,30 @@ local function bootstrap(url, ca)
if not xml then
return false, string.format('[ ta ] fetch of "%s" failed: %s', url, err)
end
local rr = ''
-- 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)
-- Parse root trust anchor, one digest at a time, converting to a zone-file-style string.
string.gsub(xml, "<KeyDigest[^>]*>(.-)</KeyDigest>", function (xml1)
local fields = {}
string.gsub(xml1, "<([%w]+).->([^<]+)</[%w]+>", function (k, v) fields[k] = v end)
rr = rr .. '\n' .. string.format('. 0 IN DS %s %s %s %s',
fields.KeyTag, fields.Algorithm, fields.DigestType, fields.Digest)
local keydigests = {}
string.gsub(xml, "<KeyDigest([^>]*)>(.-)</KeyDigest>", function(attrs, inside)
parse_xml_keydigest(attrs, inside, keydigests)
end)
if rr == '' then
return false, string.format('[ ta ] failed to get any record from "%s"', url)
local rrset = generate_ds(keydigests)
if rrset == '' then
return false, string.format('[ ta ] no valid trust anchors found at "%s"', url)
end
local msg = '[ ta ] Root trust anchors bootstrapped over https with pinned certificate.\n'
.. ' You SHOULD verify them manually against original source:\n'
.. ' https://www.iana.org/dnssec/files\n'
.. '[ ta ] Current root trust anchors are:'
.. rr
return rr, msg
.. rrset
return rrset, msg
end
-- Load the module
local ffi = require 'ffi'
local kres = require('kres')
local C = ffi.C
-- RFC5011 state table
local key_state = {
Start = 'Start', AddPend = 'AddPend', Valid = 'Valid',
......@@ -62,8 +160,14 @@ local key_state = {
-- 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)
assert(rr_tag >= 0 and rr_tag <= 65535, string.format('invalid RR: %s: %s',
kres.rr2str(rr), ffi.string(C.knot_strerror(rr_tag))))
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)
assert(ta_tag >= 0 and ta_tag <= 65535, string.format('invalid RR: %s: %s',
kres.rr2str(ta), ffi.string(C.knot_strerror(ta_tag))))
if ta.owner == rr.owner then
if ta.type == rr.type then
if rr.type == kres.type.DNSKEY then
......@@ -75,7 +179,7 @@ local function ta_find(keyset, rr)
end
-- DNSKEY superseding DS, inexact match
elseif rr.type == kres.type.DNSKEY and ta.type == kres.type.DS then
if ta.key_tag == C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata) then
if ta.key_tag == rr_tag then
keyset[i] = rr -- Replace current DS
rr.state = ta.state
rr.key_tag = ta.key_tag
......@@ -83,9 +187,7 @@ local function ta_find(keyset, rr)
end
-- DS key matching DNSKEY, inexact match
elseif rr.type == kres.type.DS and ta.type == kres.type.DNSKEY then
local ds_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
local dnskey_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
if ds_tag == dnskey_tag then
if rr_tag == ta_tag then
return ta
end
end
......@@ -103,6 +205,8 @@ local function ta_present(keyset, rr, hold_down_time, force_valid)
local now = os.time()
local key_revoked = (rr.type == kres.type.DNSKEY) and C.kr_dnssec_key_revoked(rr.rdata)
local key_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
assert(key_tag >= 0 and key_tag <= 65535, string.format('invalid RR: %s: %s',
kres.rr2str(rr), ffi.string(C.knot_strerror(key_tag))))
local ta = ta_find(keyset, rr)
if ta then
-- Key reappears (KeyPres)
......@@ -128,7 +232,7 @@ local function ta_present(keyset, rr, hold_down_time, force_valid)
ta.timer = nil
end
if rr.state ~= key_state.Valid or verbose() then
log('[ ta ] key: '..key_tag..' state: '..ta.state)
log('[ ta ] key: ' .. key_tag .. ' state: '..ta.state)
end
return true
elseif not key_revoked then -- First time seen (NewKey)
......@@ -140,7 +244,7 @@ local function ta_present(keyset, rr, hold_down_time, force_valid)
rr.timer = now + hold_down_time
end
if rr.state ~= key_state.Valid or verbose() then
log('[ ta ] key: '..key_tag..' state: '..rr.state)
log('[ ta ] key: ' .. key_tag .. ' state: '..rr.state)
end
table.insert(keyset, rr)
return true
......@@ -153,6 +257,8 @@ 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)
assert(key_tag >= 0 and key_tag <= 65535, string.format('invalid RR: %s: %s',
kres.rr2str(ta), ffi.string(C.knot_strerror(key_tag))))
if ta.state == key_state.Valid then
ta.state = key_state.Missing
ta.timer = os.time() + hold_down_time
......@@ -286,7 +392,12 @@ local function keyset_read(path)
end
for _, ta in pairs(tas) do
ta.key_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
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
end
return tas
end
......
-- check prerequisites
local has_http = pcall(require, 'http') and pcall(require, 'http.request')
if not has_http then
pass('skipping bootstrap tests because http module is not not installed')
done()
end
local cqueues = require("cqueues")
local socket = require("cqueues.socket")
-- unload modules which are not related to this test
if ta_signal_query then
modules.unload('ta_signal_query')
end
if priming then
modules.unload('priming')
end
if detect_time_skew then
modules.unload('detect_time_skew')
end
-- Self-checks on globals
assert(help() ~= nil)
assert(worker.id ~= nil)
-- Self-checks on facilities
assert(worker.stats() ~= nil)
assert(net.interfaces() ~= nil)
-- Self-checks on loaded stuff
assert(#modules.list() > 0)
-- Self-check timers
ev = event.recurrent(1 * sec, function () return 1 end)
event.cancel(ev)
ev = event.after(0, function () return 1 end)
-- do not attempt to contact outside world using DNS, operate only on cache
net.ipv4 = false
net.ipv6 = false
-- do not listen, test is driven by config code
env.KRESD_NO_LISTEN = true
-- start test webserver
local function start_webserver()
-- srvout = io.popen('luajit webserv.lua')
-- TODO
os.execute('luajit webserv.lua &')
-- assert(srvout, 'failed to start webserver')
end
local function wait_for_webserver()
local starttime = os.time()
local connected = false
while not connected and os.difftime(os.time(), starttime) < 5 do
local con = socket.connect("localhost", 8080)
connected, msg = pcall(con.connect, con, 5)
cqueues.sleep (0.3)
end
assert(connected, string.format('unable to connect to web server: %s', msg))
end
local host = 'https://localhost:8080/'
-- avoid interference with configured KEYFILE_DEFAULT
trust_anchors.keyfile_default = nil
local function test_err_cert()
trust_anchors.bootstrap_ca = 'x509/wrongca.pem'
trust_anchors.bootstrap_url = host .. 'ok1.xml'
boom(trust_anchors.add_file, {'ok1.keys'},
'fake server certificate is detected')
end
local function test_err_xml(testname, testdesc)
return function()
trust_anchors.bootstrap_ca = 'x509/ca.pem'
trust_anchors.bootstrap_url = host .. testname .. '.xml'
boom(trust_anchors.add_file, {testname .. '.keys'}, testdesc)
end
end
-- dumb test, right now it cannot check content of keys because
-- it does not get written until refresh fetches DNSKEY from network
-- (and bypassing network using policy bypasses also validation
-- so it does not test anything)
local function test_ok_xml(testname, testdesc)
return function()
trust_anchors.bootstrap_url = host .. testname .. '.xml'
same(trust_anchors.add_file(testname .. '.keys'), nil, testdesc)
end
end
return {
start_webserver,
wait_for_webserver,
test_err_cert,
test_err_xml('err_attr_extra_attr', 'bogus TA XML with an extra attribute'),
test_err_xml('err_attr_validfrom_invalid', 'bogus TA XML with invalid validFrom value'),
test_err_xml('err_attr_validfrom_missing', 'bogus TA XML without mandatory validFrom attribute'),
test_err_xml('err_elem_extra', 'bogus TA XML with an extra element'),
test_err_xml('err_elem_missing', 'bogus TA XML without mandatory element'),
test_err_xml('err_multi_ta', 'bogus TA XML with multiple TAs'),
test_err_xml('unsupp_nonroot', 'unsupported TA XML for non-root zone'),
test_err_xml('unsupp_xml_v11', 'unsupported TA XML with XML v1.1'),
test_err_xml('ok0_badtimes', 'TA XML with no valid keys'),
test_ok_xml('ok1_expired1', 'TA XML with 1 valid and 1 expired key'),
test_ok_xml('ok1_notyet1', 'TA XML with 1 valid and 1 not yet valid key'),
test_ok_xml('ok1', 'TA XML with 1 valid key'),
test_ok_xml('ok2', 'TA XML with 2 valid keys'),
}
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="FC4A93EC-9F4E-4597-A766-AD6723E4A56E" source="https://localhost/err_attr_extra_attr.xml">
<Zone>.</Zone>
<KeyDigest unknownattr="test" id="Kjqmt7v" validFrom="2010-07-15T00:00:00+00:00" validUntil="2019-01-11T00:00:00+00:00">
<KeyTag>19036</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
</KeyDigest>
<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
<KeyTag>20326</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="ABD668AB-52DF-4A59-80E3-16CE6341BC55" source="https://localhost/err_attr_validfrom_invalid.xml">
<Zone>.</Zone>
<KeyDigest id="Kjqmt7v" validFrom="2010-07-32T00:00:00+00:00" validUntil="2019-01-11T00:00:00+00:00">
<KeyTag>19036</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
</KeyDigest>
<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
<KeyTag>20326</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="3513058C-4041-40CC-AF0A-D3CCD70F962B" source="https://localhost/err_attr_validfrom_missing.xml">
<Zone>.</Zone>
<KeyDigest id="Kjqmt7v" validUntil="2019-01-11T00:00:00+00:00">
<KeyTag>19036</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
</KeyDigest>
<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
<KeyTag>20326</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="B1854D58-1867-4FA7-872F-0099D394114D" source="https://localhost/err_elem_extra.xml">
<Zone>.</Zone>
<KeyDigest id="Kjqmt7v" validFrom="2010-07-15T00:00:00+00:00" validUntil="2019-01-11T00:00:00+00:00">
<KeyTag>19036</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
</KeyDigest>
<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
<KeyTag>20326</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
<UnknownElement>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</UnknownElement>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="BB074095-3A42-4B13-9CC1-CFFF644D4D54" source="https://localhost/err_elem_missing.xml">
<Zone>.</Zone>
<KeyDigest id="Kjqmt7v" validFrom="2010-07-15T00:00:00+00:00" validUntil="2019-01-11T00:00:00+00:00">
<KeyTag>19036</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5</Digest>
</KeyDigest>
<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
<KeyTag>20326</KeyTag>
<Algorithm>8</Algorithm>
<!-- this element is missing: DigestType>2</DigestType-->
<Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="9DCE46E1-FC78-48E1-81B5-94E328790BB5" source="https://localhost/err_multi_ta.xml">
<Zone>.</Zone>
<KeyDigest id="1" validFrom="2000-02-02T00:00:00+00:00">
<KeyTag>2</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
</KeyDigest>
</TrustAnchor>
<TrustAnchor id="9DCE46E1-FC78-48E1-81B5-94E328790BB5" source="https://localhost/err_multi_ta.xml">
<Zone>test.</Zone>
<KeyDigest id="2" validFrom="2000-02-02T00:00:00+00:00">
<KeyTag>2</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="EDEDAA08-D2A0-421E-81DC-AF11F5A0CDCD" source="https://localhost/ok0_badtimes.xml">
<Zone>.</Zone>
<KeyDigest id="E" validFrom="2000-01-01T00:00:00+00:00" validUntil="2000-01-01T00:00:00+00:00">
<KeyTag>1</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE</Digest>
</KeyDigest>
<KeyDigest id="F" validFrom="2001-01-01T00:00:00+00:00" validUntil="2001-01-01T00:00:00+00:00">
<KeyTag>2</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="82E6CB77-12DF-4E61-BF49-367FB95A8BAA" source="https://localhost/ok1.xml">
<Zone>.</Zone>
<KeyDigest id="2" validFrom="2000-02-02T00:00:00+00:00">
<KeyTag>2</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="68463155-A857-4C7E-BCA6-2F6CC2EAC1BE" source="https://localhost/ok1_expired1.xml">
<Zone>.</Zone>
<KeyDigest id="F" validFrom="1990-01-01T00:00:00+00:00" validUntil="2000-01-01T00:00:00+00:00">
<KeyTag>1</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF</Digest>
</KeyDigest>
<KeyDigest id="1" validFrom="2000-01-01T00:00:00+00:00">
<KeyTag>2</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="507B39D5-049E-467C-9E9A-F5BE597C9DDA" source="https://localhost/ok1_notyet1.xml">
<Zone>.</Zone>
<KeyDigest id="1" validFrom="2010-07-15T00:00:00+00:00">
<KeyTag>1</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
</KeyDigest>
<KeyDigest id="2" validFrom="2050-12-31T23:59:59+00:00">
<KeyTag>2</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="1DECEB91-0591-44A1-95CF-1788337514B8" source="https://localhost/ok2.xml">
<Zone>.</Zone>
<KeyDigest id="K1" validFrom="2010-07-15T00:00:00+00:00">
<KeyTag>1</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
</KeyDigest>
<KeyDigest id="K2" validFrom="2011-02-02T00:00:00+00:00">
<KeyTag>2</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>2222222222222222222222222222222222222222222222222222222222222222</Digest>
</KeyDigest>
</TrustAnchor>
for F in *.xml; do sed -i "s/TrustAnchor id=\"[^\"]*\"/TrustAnchor id=\"$(uuidgen | tr '[[:lower:]]' '[[:upper:]]')\"/" $F; done
for F in *.xml; do sed -i "s#source=\"[^\"]*\"#source=\"https://localhost/$F\"#" $F; done
<?xml version="1.0" encoding="UTF-8"?>
<TrustAnchor id="8449BFB8-FD6C-4082-B0FE-1A3E3399203B" source="https://localhost/unsupp_nonroot.xml">
<Zone>test.</Zone>
<KeyDigest id="2" validFrom="2000-02-02T00:00:00+00:00">
<KeyTag>2</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
</KeyDigest>
</TrustAnchor>
<?xml version="1.1" encoding="UTF-8"?>
<TrustAnchor id="3612AE1C-E8F3-4FD8-B8CD-96C7FDACC7A5" source="https://localhost/unsupp_xml_v11.xml">
<Zone>.</Zone>
<KeyDigest id="2" validFrom="2000-02-02T00:00:00+00:00">
<KeyTag>2</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>1111111111111111111111111111111111111111111111111111111111111111</Digest>
</KeyDigest>
</TrustAnchor>
-- This is a module that does the heavy lifting to provide an HTTP/2 enabled
-- server that supports TLS by default and provides endpoint for other modules
-- in order to enable them to export restful APIs and websocket streams.
-- One example is statistics module that can stream live metrics on the website,
-- or publish metrics on request for Prometheus scraper.
local http_server = require('http.server')
local http_headers = require('http.headers')
local http_websocket = require('http.websocket')
local http_util = require "http.util"
local x509, pkey = require('openssl.x509'), require('openssl.pkey')
-- Module declaration
local M = {}
-- Export HTTP service endpoints
M.endpoints = {
['/'] = {'text/html', 'test'},
}
-- Serve known requests, for methods other than GET
-- the endpoint must be a closure and not a preloaded string
local function serve(endpoints, h, stream)
local hsend = http_headers.new()
local path = h:get(':path')
local entry = endpoints[path]
if not entry then -- Accept top-level path match
entry = endpoints[path:match '^/[^/?]*']
end
-- Unpack MIME and data
local data, mime, ttl, err
if entry then
mime = entry[1]
data = entry[2]
ttl = entry[4]
end
-- Get string data out of service endpoint
if type(data) == 'function' then
local set_mime, set_ttl
data, err, set_mime, set_ttl = data(h, stream)
-- Override default endpoint mime/TTL
if set_mime then mime = set_mime end
if set_ttl then ttl = set_ttl end
-- Handler doesn't provide any data
if data == false then return end
if type(data) == 'number' then return tostring(data), err end
-- Methods other than GET require handler to be closure
elseif h:get(':method') ~= 'GET' then
return '501', ''
end
if not mime or type(data) ~= 'string' then
return '404', ''
else
-- Serve content type appropriately
hsend:append(':status', '200')
hsend:append('content-type', mime)
hsend:append('content-length', tostring(#data))
if ttl then
hsend:append('cache-control', string.format('max-age=%d', ttl))
end
assert(stream:write_headers(hsend, false))
assert(stream:write_chunk(data, true))
end
end
-- Web server service closure
local function route(endpoints)
return function (_, stream)
-- HTTP/2: We're only permitted to send in open/half-closed (remote)
local connection = stream.connection
if connection.version >= 2 then
if stream.state ~= 'open' and stream.state ~= 'half closed (remote)' then
return
end
end
-- Start reading headers
local h = assert(stream:get_headers())
local m = h:get(':method')
local path = h:get(':path')
-- Upgrade connection to WebSocket
local ws = http_websocket.new_from_stream(stream, h)
if ws then
assert(ws:accept { protocols = {'json'} })
-- Continue streaming results to client
local ep = endpoints[path]
local cb = ep[3]
if cb then
cb(h, ws)
end
ws:close()
return
else
local ok, err, reason = http_util.yieldable_pcall(serve, endpoints, h, stream)
if not ok or err then
print(string.format('%s err %s %s: %s (%s)', os.date(), m, path, err or '500', reason))
-- Method is not supported
local hsend = http_headers.new()
hsend:append(':status', err or '500')
if reason then
assert(stream:write_headers(hsend, false))
assert(stream:write_chunk(reason, true))
else
assert(stream:write_headers(hsend, true))
end
else
print(string.format('%s ok %s %s', os.date(), m, path))
end
end
end
end
-- @function Prefer HTTP/2 or HTTP/1.1
local function alpnselect(_, protos)
for _, proto in ipairs(protos) do
if proto == 'h2' or proto == 'http/1.1' then
return proto
end
end
return nil
end
-- @function Create TLS context
local function tlscontext(crt, key)
local http_tls = require('http.tls')
local ctx = http_tls.new_server_context()
if ctx.setAlpnSelect then
ctx:setAlpnSelect(alpnselect)
end
assert(ctx:setPrivateKey(key))
assert(ctx:setCertificate(crt))
return ctx
end
-- @function Listen on given HTTP(s) host
function M.add_interface(conf)
local crt, key
if conf.tls ~= false then
assert(conf.cert,