]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
trust_anchors: respect timestamps in root-anchors.xml
authorPetr Špaček <petr.spacek@nic.cz>
Fri, 21 Dec 2018 15:28:27 +0000 (16:28 +0100)
committerPetr Špaček <petr.spacek@nic.cz>
Wed, 9 Jan 2019 14:28:39 +0000 (15:28 +0100)
We are not RFC 7958 compliant and support only XML with just root zone
TA. Full compliance would require either proper Lua XML parser or CMS parser
and both are hard to get packaged in Fedora and elsewhere.

Also timestamps related to TA validity are limited to UTC timezone
because cross-platform timezone parsing is hard.
(Mac OS libc does not have usable strptime(%z).)

Closes: #435
daemon/lua/trust_anchors.lua.in

index 76f5139cd1d1c7f34d0f8862c47732135dd4afdd..7e28bc31ab0251f69411b294db4bfc0efb6b3671 100644 (file)
@@ -1,3 +1,8 @@
+-- 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,93 @@ 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))
+
+       _, 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 +122,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',