From: Marek Vavruša Date: Sat, 5 Dec 2015 17:09:23 +0000 (+0100) Subject: daemon: root trust anchors automatically bootstrapped from IANA X-Git-Tag: v1.0.0-beta3~49 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1af623da2463bed31cd12b014b1e232639b0fe3f;p=thirdparty%2Fknot-resolver.git daemon: root trust anchors automatically bootstrapped from IANA if the root key file doesn’t exist, it will be populated from root DNSKEY query, which will be validated against root trust anchors retrieved over HTTPS with IANA cert verification against built-in current IANA cert CA. it requires luasocket and luasec for it to work. trust anchors XML file signature is not checked, as there’s no facility for PKCS7 checking yet. --- diff --git a/Makefile b/Makefile index a1158cef4..298429deb 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ include platform.mk # Targets all: info lib daemon modules -install: lib-install daemon-install modules-install +install: lib-install daemon-install modules-install etc-install check: all tests clean: lib-clean daemon-clean modules-clean tests-clean doc-clean doc: doc-html @@ -53,6 +53,7 @@ info: $(info PREFIX: $(PREFIX)) $(info BINDIR: $(BINDIR)) $(info LIBDIR: $(LIBDIR)) + $(info ETCDIR: $(ETCDIR)) $(info INCLUDEDIR: $(INCLUDEDIR)) $(info MODULEDIR: $(MODULEDIR)) $(info ) @@ -73,10 +74,13 @@ info: $(info [$(HAS_socket_wrapper)] socket_wrapper (lib)) $(info ) -# Moduledir +# Installation directories $(PREFIX)/$(MODULEDIR): - $(INSTALL) -d $(PREFIX)/$(MODULEDIR) + $(INSTALL) -d $@ moduledir: $(PREFIX)/$(MODULEDIR) +$(PREFIX)/$(ETCDIR): + $(INSTALL) -m 0750 -d $@ +etcdir: $(PREFIX)/$(ETCDIR) # Sub-targets include lib/lib.mk @@ -84,3 +88,4 @@ include daemon/daemon.mk include modules/modules.mk include tests/tests.mk include doc/doc.mk +include etc/etc.mk diff --git a/config.mk b/config.mk index db7d462ba..c7c2e51d1 100644 --- a/config.mk +++ b/config.mk @@ -9,12 +9,13 @@ BINDIR := /bin LIBDIR := /lib INCLUDEDIR := /include MODULEDIR := $(LIBDIR)/kdns_modules +ETCDIR := /etc/kresd # Tools CC ?= cc BUILD_LDFLAGS += $(LDFLAGS) BUILD_CFLAGS := $(CFLAGS) -std=c99 -D_GNU_SOURCE -fPIC -Wtype-limits -Wall -I$(abspath .) -I$(abspath lib/generic) -I$(abspath contrib) -BUILD_CFLAGS += -DPACKAGE_VERSION="\"$(MAJOR).$(MINOR).$(PATCH)\"" -DPREFIX="\"$(PREFIX)\"" -DMODULEDIR="\"$(MODULEDIR)\"" +BUILD_CFLAGS += -DPACKAGE_VERSION="\"$(MAJOR).$(MINOR).$(PATCH)\"" -DPREFIX="\"$(PREFIX)\"" -DMODULEDIR="\"$(MODULEDIR)\"" -DETCDIR="\"$(ETCDIR)\"" RM := rm -f LN := ln -s XXD := ./scripts/embed.sh diff --git a/daemon/README.rst b/daemon/README.rst index 6e5a2bc59..72d42cefd 100644 --- a/daemon/README.rst +++ b/daemon/README.rst @@ -14,9 +14,34 @@ Enabling DNSSEC =============== The resolver supports DNSSEC including :rfc:`5011` automated DNSSEC TA updates and :rfc:`7646` negative trust anchors. -To enable it, you need to provide at least _one_ trust anchor. This step is not automatic, as you're supposed to obtain -the trust anchor `using a secure channel `_. -From there, the Knot DNS Resolver can perform automatic updates for you. +To enable it, you need to provide trusted root keys. Bootstrapping of the keys is automated, and kresd fetches root trust anchors set `over a secure channel `_ from IANA. From there, it can perform :rfc:`5011` automatic updates for you. + +.. note:: Automatic bootstrap requires luasocket_ and luasec_ installed. + +.. code-block:: bash + + $ kresd -k root.keys # File for root keys + [ ta ] bootstrapped root anchor "19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5" + [ ta ] warning: you SHOULD check the key manually, see: https://data.iana.org/root-anchors/draft-icann-dnssec-trust-anchor.html#sigs + [ ta ] key: 19036 state: Valid + [ ta ] next refresh: 86400000 + +Alternatively, you can set it in configuration file with ``trust_anchors.file = 'root.keys'``. If the file doesn't exist, it will be automatically populated with root keys validated using root anchors retrieved over HTTPS. + +This is equivalent to `using unbound-anchor `_: + +.. code-block:: bash + + $ unbound-anchor -a "root.keys" || echo "warning: check the key at this point" + $ echo "auto-trust-anchor-file: \"root.keys\"" >> unbound.conf + $ unbound -c unbound.conf + +.. warning:: Bootstrapping of the root trust anchors is automatic, you are however **encouraged to check** the key over **secure channel**, as specified in `DNSSEC Trust Anchor Publication for the Root Zone `_. This is a critical step where the whole infrastructure may be compromised, you will be warned in the server log. + +Manually providing root anchors +------------------------------- + +The root anchors bootstrap may fail for various reasons, in this case you need to provide IANA or alternative root anchors. The format of the keyfile is the same as for Unbound or BIND and contains DNSKEY records. 1. Check the current TA published on `IANA website `_ 2. Fetch current keys (DNSKEY), verify digests @@ -714,4 +739,6 @@ you can see the statistics or schedule new queries. .. _LuaRocks: https://rocks.moonscript.org/ .. _libuv: https://github.com/libuv/libuv .. _Lua: http://www.lua.org/about.html -.. _LuaJIT: http://luajit.org/luajit.html \ No newline at end of file +.. _LuaJIT: http://luajit.org/luajit.html +.. _luasec: https://luarocks.org/modules/luarocks/luasec +.. _luasocket: https://luarocks.org/modules/luarocks/luasocket \ No newline at end of file diff --git a/daemon/engine.c b/daemon/engine.c index 8d92c13da..78eb0a466 100644 --- a/daemon/engine.c +++ b/daemon/engine.c @@ -396,6 +396,10 @@ static int init_state(struct engine *engine) lua_setglobal(engine->L, "user"); lua_pushcfunction(engine->L, l_libpath); lua_setglobal(engine->L, "libpath"); + lua_pushliteral(engine->L, PREFIX MODULEDIR); + lua_setglobal(engine->L, "moduledir"); + lua_pushliteral(engine->L, PREFIX ETCDIR); + lua_setglobal(engine->L, "etcdir"); lua_pushlightuserdata(engine->L, engine); lua_setglobal(engine->L, "__engine"); return kr_ok(); diff --git a/daemon/lua/trust_anchors.lua b/daemon/lua/trust_anchors.lua index 7e0495730..d6e91340c 100644 --- a/daemon/lua/trust_anchors.lua +++ b/daemon/lua/trust_anchors.lua @@ -52,7 +52,7 @@ local function ta_present(keyset, rr, hold_down_time, force) ta.state = key_state.Valid ta.timer = nil end - print('[trust_anchors] key: '..key_tag..' state: '..ta.state) + print('[ ta ] key: '..key_tag..' state: '..ta.state) return true elseif not key_revoked then -- First time seen (NewKey) rr.key_tag = key_tag @@ -62,7 +62,7 @@ local function ta_present(keyset, rr, hold_down_time, force) rr.state = key_state.AddPend rr.timer = now + hold_down_time end - print('[trust_anchors] key: '..key_tag..' state: '..rr.state) + print('[ ta ] key: '..key_tag..' state: '..rr.state) table.insert(keyset, rr) return true end @@ -79,24 +79,24 @@ local function ta_missing(keyset, ta, hold_down_time) ta.timer = os.time() + hold_down_time -- Purge pending key elseif ta.state == key_state.AddPend then - print('[trust_anchors] key: '..key_tag..' purging') + print('[ ta ] key: '..key_tag..' purging') keep_ta = false end - print('[trust_anchors] key: '..key_tag..' state: '..ta.state) + print('[ ta ] key: '..key_tag..' state: '..ta.state) return keep_ta end -- Plan refresh event and re-schedule itself based on the result of the callback -local function refresh_plan(trust_anchors, timeout, refresh_cb, priming) +local function refresh_plan(trust_anchors, timeout, refresh_cb, priming, bootstrap) trust_anchors.refresh_ev = event.after(timeout, function (ev) resolve('.', kres.type.DNSKEY, kres.class.IN, kres.query.NO_CACHE, function (pkt) -- Schedule itself with updated timeout - local next_time = refresh_cb(trust_anchors, kres.pkt_t(pkt)) + local next_time = refresh_cb(trust_anchors, kres.pkt_t(pkt), bootstrap) if trust_anchors.refresh_time ~= nil then next_time = math.min(next_time, trust_anchors.refresh_time) end - print('[trust_anchors] next refresh: '..next_time) + print('[ ta ] next refresh: '..next_time) refresh_plan(trust_anchors, next_time, refresh_cb) -- Priming query, prime root NS next if priming ~= nil then @@ -107,7 +107,7 @@ local function refresh_plan(trust_anchors, timeout, refresh_cb, priming) end -- Active refresh, return time of the next check -local function active_refresh(trust_anchors, pkt) +local function active_refresh(trust_anchors, pkt, bootstrap) local retry = true if pkt:rcode() == kres.rcode.NOERROR then local records = pkt:section(kres.section.ANSWER) @@ -117,7 +117,7 @@ local function active_refresh(trust_anchors, pkt) table.insert(keyset, rr) end end - trust_anchors.update(keyset, false) + trust_anchors.update(keyset, bootstrap) retry = false end -- Calculate refresh/retry timer (RFC 5011, 2.3) @@ -143,6 +143,22 @@ local function keyset_write(keyset, path) os.rename(path..'.lock', path) end +-- Fetch over HTTPS with peert cert checked +local function https_fetch(url, ca) + local https = require('ssl.https') + local ltn12 = require('ltn12') + 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 + -- TA store management local trust_anchors = { keyset = {}, @@ -187,21 +203,31 @@ local trust_anchors = { return true end, -- Load keys from a file (managed) - config = function (path, is_unmanaged) - if path == trust_anchors.file_current then return end + config = function (path, unmanaged, bootstrap) + bootstrap = true + -- Bootstrap if requested and keyfile doesn't exist + if bootstrap and not io.open(path, 'r') then + if not trust_anchors.bootstrap() then + error('you MUST obtain the root TA manually, see: '.. + 'http://knot-resolver.readthedocs.org/en/latest/daemon.html#enabling-dnssec') + end + elseif path == trust_anchors.file_current then + return + end + -- Parse new keys local new_keys = require('zonefile').parse_file(path) trust_anchors.file_current = path - if is_unmanaged then trust_anchors.file_current = nil end + if unmanaged then trust_anchors.file_current = nil end trust_anchors.keyset = {} - if trust_anchors.update(new_keys, true) then + if bootstrap or trust_anchors.update(new_keys, true) then if trust_anchors.refresh_ev ~= nil then event.cancel(trust_anchors.refresh_ev) end - refresh_plan(trust_anchors, sec, active_refresh, true) + refresh_plan(trust_anchors, sec, active_refresh, true, bootstrap) end end, -- Add DS/DNSKEY record(s) (unmanaged) add = function (keystr) local store = kres.context().trust_anchors - require('zonefile').parser(function (p) + return require('zonefile').parser(function (p) local rr = p:current_rr() C.kr_ta_add(store, rr.owner, rr.type, rr.ttl, rr.rdata, #rr.rdata) end):read(keystr..'\n') @@ -216,6 +242,33 @@ local trust_anchors = { end trust_anchors.insecure = list end, + bootstrap = function (url, ca) + -- Fetch root anchors in XML over HTTPS + -- @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 + print(string.format('[ ta ] fetch of "%s" failed: %s', url, err)) + return false + end + -- Parse root trust anchor + local fields = {} + string.gsub(xml, "<([%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 + if trust_anchors.add(rr) ~= 0 then + print(string.format('[ ta ] invalid format of the RR "%s"', rr)) + return false + end + print(string.format('[ ta ] bootstrapped root anchor "%s"', rrdata)) + print('[ ta ] warning: you SHOULD check the key manually, see: '.. + 'https://data.iana.org/root-anchors/draft-icann-dnssec-trust-anchor.html#sigs') + return true + end, } return trust_anchors \ No newline at end of file diff --git a/daemon/main.c b/daemon/main.c index 519418a18..37f9ae0f3 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -242,14 +242,29 @@ int main(int argc, char **argv) #endif break; case 'k': - keyfile_buf = malloc(PATH_MAX + 1); + keyfile_buf = malloc(PATH_MAX); assert(keyfile_buf); - keyfile = realpath(optarg, keyfile_buf); - if (keyfile) - keyfile = strdup(keyfile); + /* Check if the path is absolute */ + if (optarg[0] == '/') { + keyfile = strdup(optarg); + } else { + /* Construct absolute path, the file may not exist */ + keyfile = realpath(".", keyfile_buf); + if (keyfile) { + int len = strlen(keyfile); + int namelen = strlen(optarg); + if (len + namelen < PATH_MAX - 1) { + keyfile[len] = '/'; + memcpy(keyfile + len + 1, optarg, namelen + 1); + keyfile = strdup(keyfile); /* Duplicate */ + } else { + keyfile = NULL; /* Invalidate */ + } + } + } free(keyfile_buf); - if (!keyfile || access(optarg, R_OK|W_OK) != 0) { - log_error("[system] keyfile '%s': not readable/writeable\n", optarg); + if (!keyfile) { + log_error("[system] keyfile '%s': not writeable\n", optarg); return EXIT_FAILURE; } break; diff --git a/doc/build.rst b/doc/build.rst index 98407581f..1526ac233 100644 --- a/doc/build.rst +++ b/doc/build.rst @@ -39,6 +39,8 @@ There are also *optional* packages that enable specific functionality in Knot DN .. csv-table:: :header: "Optional", "Needed for", "Notes" + "luasocket_", "``trust anchors, modules/stats``", "Sockets for Lua." + "luasec_", "``trust anchors``", "TLS for Lua." "libmemcached_", "``modules/memcached``", "To build memcached backend module." "hiredis_", "``modules/redis``", "To build redis backend module." "Go_ 1.5+", "``modules``", "Build modules written in Go." @@ -186,6 +188,8 @@ Read the `documentation `_ for more information about requirements, .. _libknot: https://gitlab.labs.nic.cz/labs/knot .. _cmocka: https://cmocka.org/ .. _Python: https://www.python.org/ +.. _luasec: https://luarocks.org/modules/luarocks/luasec +.. _luasocket: https://luarocks.org/modules/luarocks/luasocket .. _boot2docker: http://boot2docker.io/ diff --git a/etc/etc.mk b/etc/etc.mk new file mode 100644 index 000000000..4a5a428fa --- /dev/null +++ b/etc/etc.mk @@ -0,0 +1,6 @@ +etc_SOURCES := icann-ca.pem + +etc-install: etcdir + $(INSTALL) -m 0640 $(addprefix etc/,$(etc_SOURCES)) $(PREFIX)/$(ETCDIR) + +.PHONY: etc-install diff --git a/etc/icann-ca.pem b/etc/icann-ca.pem new file mode 100644 index 000000000..0ae28d651 --- /dev/null +++ b/etc/icann-ca.pem @@ -0,0 +1,82 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 08:3b:e0:56:90:42:46:b1:a1:75:6a:c9:59:91:c7:4a + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root CA + Validity + Not Before: Nov 10 00:00:00 2006 GMT + Not After : Nov 10 00:00:00 2031 GMT + Subject: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root CA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (2048 bit) + Modulus (2048 bit): + 00:e2:3b:e1:11:72:de:a8:a4:d3:a3:57:aa:50:a2: + 8f:0b:77:90:c9:a2:a5:ee:12:ce:96:5b:01:09:20: + cc:01:93:a7:4e:30:b7:53:f7:43:c4:69:00:57:9d: + e2:8d:22:dd:87:06:40:00:81:09:ce:ce:1b:83:bf: + df:cd:3b:71:46:e2:d6:66:c7:05:b3:76:27:16:8f: + 7b:9e:1e:95:7d:ee:b7:48:a3:08:da:d6:af:7a:0c: + 39:06:65:7f:4a:5d:1f:bc:17:f8:ab:be:ee:28:d7: + 74:7f:7a:78:99:59:85:68:6e:5c:23:32:4b:bf:4e: + c0:e8:5a:6d:e3:70:bf:77:10:bf:fc:01:f6:85:d9: + a8:44:10:58:32:a9:75:18:d5:d1:a2:be:47:e2:27: + 6a:f4:9a:33:f8:49:08:60:8b:d4:5f:b4:3a:84:bf: + a1:aa:4a:4c:7d:3e:cf:4f:5f:6c:76:5e:a0:4b:37: + 91:9e:dc:22:e6:6d:ce:14:1a:8e:6a:cb:fe:cd:b3: + 14:64:17:c7:5b:29:9e:32:bf:f2:ee:fa:d3:0b:42: + d4:ab:b7:41:32:da:0c:d4:ef:f8:81:d5:bb:8d:58: + 3f:b5:1b:e8:49:28:a2:70:da:31:04:dd:f7:b2:16: + f2:4c:0a:4e:07:a8:ed:4a:3d:5e:b5:7f:a3:90:c3: + af:27 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Digital Signature, Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55 + X509v3 Authority Key Identifier: + keyid:03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55 + + Signature Algorithm: sha1WithRSAEncryption + cb:9c:37:aa:48:13:12:0a:fa:dd:44:9c:4f:52:b0:f4:df:ae: + 04:f5:79:79:08:a3:24:18:fc:4b:2b:84:c0:2d:b9:d5:c7:fe: + f4:c1:1f:58:cb:b8:6d:9c:7a:74:e7:98:29:ab:11:b5:e3:70: + a0:a1:cd:4c:88:99:93:8c:91:70:e2:ab:0f:1c:be:93:a9:ff: + 63:d5:e4:07:60:d3:a3:bf:9d:5b:09:f1:d5:8e:e3:53:f4:8e: + 63:fa:3f:a7:db:b4:66:df:62:66:d6:d1:6e:41:8d:f2:2d:b5: + ea:77:4a:9f:9d:58:e2:2b:59:c0:40:23:ed:2d:28:82:45:3e: + 79:54:92:26:98:e0:80:48:a8:37:ef:f0:d6:79:60:16:de:ac: + e8:0e:cd:6e:ac:44:17:38:2f:49:da:e1:45:3e:2a:b9:36:53: + cf:3a:50:06:f7:2e:e8:c4:57:49:6c:61:21:18:d5:04:ad:78: + 3c:2c:3a:80:6b:a7:eb:af:15:14:e9:d8:89:c1:b9:38:6c:e2: + 91:6c:8a:ff:64:b9:77:25:57:30:c0:1b:24:a3:e1:dc:e9:df: + 47:7c:b5:b4:24:08:05:30:ec:2d:bd:0b:bf:45:bf:50:b9:a9: + f3:eb:98:01:12:ad:c8:88:c6:98:34:5f:8d:0a:3c:c6:e9:d5: + 95:95:6d:de +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE-----