summaryrefslogtreecommitdiffstats
path: root/daemon/lua
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--daemon/lua/config.lua35
-rw-r--r--daemon/lua/kres-gen.lua430
-rwxr-xr-xdaemon/lua/kres-gen.sh216
-rw-r--r--daemon/lua/kres.lua952
-rw-r--r--daemon/lua/sandbox.lua458
-rw-r--r--daemon/lua/trust_anchors.lua.in663
-rw-r--r--daemon/lua/trust_anchors.test/bootstrap.test.lua108
-rw-r--r--daemon/lua/trust_anchors.test/err_attr_extra_attr.xml16
-rw-r--r--daemon/lua/trust_anchors.test/err_attr_validfrom_invalid.xml16
-rw-r--r--daemon/lua/trust_anchors.test/err_attr_validfrom_missing.xml16
-rw-r--r--daemon/lua/trust_anchors.test/err_elem_extra.xml17
-rw-r--r--daemon/lua/trust_anchors.test/err_elem_missing.xml16
-rw-r--r--daemon/lua/trust_anchors.test/err_multi_ta.xml19
-rw-r--r--daemon/lua/trust_anchors.test/ok0_badtimes.xml16
-rw-r--r--daemon/lua/trust_anchors.test/ok1.xml10
-rw-r--r--daemon/lua/trust_anchors.test/ok1_expired1.xml16
-rw-r--r--daemon/lua/trust_anchors.test/ok1_notyet1.xml16
-rw-r--r--daemon/lua/trust_anchors.test/ok2.xml16
-rwxr-xr-xdaemon/lua/trust_anchors.test/regen.sh2
-rw-r--r--daemon/lua/trust_anchors.test/unsupp_nonroot.xml10
-rw-r--r--daemon/lua/trust_anchors.test/unsupp_xml_v11.xml10
-rw-r--r--daemon/lua/trust_anchors.test/webserv.lua234
-rw-r--r--daemon/lua/trust_anchors.test/x509/ca-key.pem182
-rw-r--r--daemon/lua/trust_anchors.test/x509/ca.pem24
-rw-r--r--daemon/lua/trust_anchors.test/x509/ca.tmpl3
-rwxr-xr-xdaemon/lua/trust_anchors.test/x509/gen.sh11
-rw-r--r--daemon/lua/trust_anchors.test/x509/server-key.pem182
-rw-r--r--daemon/lua/trust_anchors.test/x509/server.pem26
-rw-r--r--daemon/lua/trust_anchors.test/x509/server.tmpl6
-rw-r--r--daemon/lua/trust_anchors.test/x509/wrongca-key.pem182
-rw-r--r--daemon/lua/trust_anchors.test/x509/wrongca.pem24
-rw-r--r--daemon/lua/trust_anchors.test/x509/wrongca.tmpl3
-rw-r--r--daemon/lua/zonefile.lua91
33 files changed, 4026 insertions, 0 deletions
diff --git a/daemon/lua/config.lua b/daemon/lua/config.lua
new file mode 100644
index 0000000..448d6dd
--- /dev/null
+++ b/daemon/lua/config.lua
@@ -0,0 +1,35 @@
+-- Listen on localhost
+if not next(net.list()) and not env.KRESD_NO_LISTEN then
+ local ok, err = pcall(net.listen, '127.0.0.1')
+ if not ok then
+ error('bind to 127.0.0.1@53 '..err)
+ end
+ -- IPv6 loopback may fail
+ ok, err = pcall(net.listen, '::1')
+ if not ok and verbose() then
+ print('bind to ::1@53 '..err)
+ end
+ -- Exit when kresd isn't listening on any interfaces
+ if not next(net.list()) then
+ panic('not listening on any interface, exiting...')
+ end
+end
+-- Open cache if not set/disabled
+if not cache.current_size then
+ cache.size = 100 * MB
+end
+
+-- If no addresses for root servers are set, load them from the default file
+if require('ffi').C.kr_zonecut_is_empty(kres.context().root_hints) then
+ _hint_root_file()
+end
+
+if not trust_anchors.keysets['\0'] and trust_anchors.keyfile_default then
+ if io.open(trust_anchors.keyfile_default, 'r') then
+ trust_anchors.config(trust_anchors.keyfile_default, true)
+ else
+ panic("cannot open default trust anchor file:'%s'",
+ trust_anchors.keyfile_default
+ )
+ end
+end
diff --git a/daemon/lua/kres-gen.lua b/daemon/lua/kres-gen.lua
new file mode 100644
index 0000000..eeb8ff7
--- /dev/null
+++ b/daemon/lua/kres-gen.lua
@@ -0,0 +1,430 @@
+local ffi = require('ffi')
+--[[ This file is generated by ./kres-gen.sh ]] ffi.cdef[[
+
+typedef struct knot_dump_style knot_dump_style_t;
+extern const knot_dump_style_t KNOT_DUMP_STYLE_DEFAULT;
+typedef void knot_db_t;
+struct kr_cdb_api {};
+struct lru {};
+
+typedef struct knot_mm {
+ void *ctx, *alloc, *free;
+} knot_mm_t;
+
+typedef void *(*map_alloc_f)(void *, size_t);
+typedef void (*map_free_f)(void *baton, void *ptr);
+typedef void (*trace_log_f) (const struct kr_query *, const char *, const char *);
+typedef void (*trace_callback_f)(struct kr_request *);
+typedef enum {KNOT_ANSWER, KNOT_AUTHORITY, KNOT_ADDITIONAL} knot_section_t;
+typedef struct {
+ uint16_t pos;
+ uint16_t flags;
+ uint16_t compress_ptr[16];
+} knot_rrinfo_t;
+typedef unsigned char knot_dname_t;
+typedef struct {
+ uint16_t len;
+ uint8_t data[];
+} knot_rdata_t;
+typedef struct {
+ uint16_t count;
+ knot_rdata_t *rdata;
+} knot_rdataset_t;
+typedef struct {
+ knot_dname_t *_owner;
+ uint32_t _ttl;
+ uint16_t type;
+ uint16_t rclass;
+ knot_rdataset_t rrs;
+ void *additional;
+} knot_rrset_t;
+typedef struct knot_pkt knot_pkt_t;
+typedef struct {
+ uint8_t *ptr[15];
+} knot_edns_options_t;
+typedef struct {
+ knot_pkt_t *pkt;
+ uint16_t pos;
+ uint16_t count;
+} knot_pktsection_t;
+struct knot_compr {
+ uint8_t *wire;
+ knot_rrinfo_t *rrinfo;
+ struct {
+ uint16_t pos;
+ uint8_t labels;
+ } suffix;
+};
+typedef struct knot_compr knot_compr_t;
+struct knot_pkt {
+ uint8_t *wire;
+ size_t size;
+ size_t max_size;
+ size_t parsed;
+ uint16_t reserved;
+ uint16_t qname_size;
+ uint16_t rrset_count;
+ uint16_t flags;
+ knot_rrset_t *opt_rr;
+ knot_rrset_t *tsig_rr;
+ knot_edns_options_t *edns_opts;
+ struct {
+ uint8_t *pos;
+ size_t len;
+ } tsig_wire;
+ knot_section_t current;
+ knot_pktsection_t sections[3];
+ size_t rrset_allocd;
+ knot_rrinfo_t *rr_info;
+ knot_rrset_t *rr;
+ knot_mm_t mm;
+ knot_compr_t compr;
+};
+typedef struct {
+ void *root;
+ struct knot_mm *pool;
+} map_t;
+struct kr_qflags {
+ _Bool NO_MINIMIZE : 1;
+ _Bool NO_THROTTLE : 1;
+ _Bool NO_IPV6 : 1;
+ _Bool NO_IPV4 : 1;
+ _Bool TCP : 1;
+ _Bool RESOLVED : 1;
+ _Bool AWAIT_IPV4 : 1;
+ _Bool AWAIT_IPV6 : 1;
+ _Bool AWAIT_CUT : 1;
+ _Bool SAFEMODE : 1;
+ _Bool CACHED : 1;
+ _Bool NO_CACHE : 1;
+ _Bool EXPIRING : 1;
+ _Bool ALLOW_LOCAL : 1;
+ _Bool DNSSEC_WANT : 1;
+ _Bool DNSSEC_BOGUS : 1;
+ _Bool DNSSEC_INSECURE : 1;
+ _Bool DNSSEC_CD : 1;
+ _Bool STUB : 1;
+ _Bool ALWAYS_CUT : 1;
+ _Bool DNSSEC_WEXPAND : 1;
+ _Bool PERMISSIVE : 1;
+ _Bool STRICT : 1;
+ _Bool BADCOOKIE_AGAIN : 1;
+ _Bool CNAME : 1;
+ _Bool REORDER_RR : 1;
+ _Bool TRACE : 1;
+ _Bool NO_0X20 : 1;
+ _Bool DNSSEC_NODS : 1;
+ _Bool DNSSEC_OPTOUT : 1;
+ _Bool NONAUTH : 1;
+ _Bool FORWARD : 1;
+ _Bool DNS64_MARK : 1;
+ _Bool CACHE_TRIED : 1;
+ _Bool NO_NS_FOUND : 1;
+};
+typedef struct {
+ knot_rrset_t **at;
+ size_t len;
+ size_t cap;
+} rr_array_t;
+struct ranked_rr_array_entry {
+ uint32_t qry_uid;
+ uint8_t rank;
+ uint8_t revalidation_cnt;
+ _Bool cached : 1;
+ _Bool yielded : 1;
+ _Bool to_wire : 1;
+ _Bool expiring : 1;
+ knot_rrset_t *rr;
+};
+typedef struct ranked_rr_array_entry ranked_rr_array_entry_t;
+typedef struct {
+ ranked_rr_array_entry_t **at;
+ size_t len;
+ size_t cap;
+} ranked_rr_array_t;
+typedef struct trie trie_t;
+struct kr_zonecut {
+ knot_dname_t *name;
+ knot_rrset_t *key;
+ knot_rrset_t *trust_anchor;
+ struct kr_zonecut *parent;
+ trie_t *nsset;
+ knot_mm_t *pool;
+};
+typedef struct {
+ struct kr_query **at;
+ size_t len;
+ size_t cap;
+} kr_qarray_t;
+struct kr_rplan {
+ kr_qarray_t pending;
+ kr_qarray_t resolved;
+ struct kr_request *request;
+ knot_mm_t *pool;
+ uint32_t next_uid;
+};
+struct kr_request_qsource_flags {
+ _Bool tcp : 1;
+ _Bool tls : 1;
+};
+struct kr_request {
+ struct kr_context *ctx;
+ knot_pkt_t *answer;
+ struct kr_query *current_query;
+ struct {
+ const struct sockaddr *addr;
+ const struct sockaddr *dst_addr;
+ const knot_pkt_t *packet;
+ struct kr_request_qsource_flags flags;
+ size_t size;
+ } qsource;
+ struct {
+ unsigned int rtt;
+ const struct sockaddr *addr;
+ } upstream;
+ struct kr_qflags options;
+ int state;
+ ranked_rr_array_t answ_selected;
+ ranked_rr_array_t auth_selected;
+ ranked_rr_array_t add_selected;
+ rr_array_t additional;
+ _Bool answ_validated;
+ _Bool auth_validated;
+ uint8_t rank;
+ struct kr_rplan rplan;
+ trace_log_f trace_log;
+ trace_callback_f trace_finish;
+ int vars_ref;
+ knot_mm_t pool;
+ unsigned int uid;
+ void *daemon_context;
+};
+enum kr_rank {KR_RANK_INITIAL, KR_RANK_OMIT, KR_RANK_TRY, KR_RANK_INDET = 4, KR_RANK_BOGUS, KR_RANK_MISMATCH, KR_RANK_MISSING, KR_RANK_INSECURE, KR_RANK_AUTH = 16, KR_RANK_SECURE = 32};
+struct kr_cache {
+ knot_db_t *db;
+ const struct kr_cdb_api *api;
+ struct {
+ uint32_t hit;
+ uint32_t miss;
+ uint32_t insert;
+ uint32_t delete;
+ } stats;
+ uint32_t ttl_min;
+ uint32_t ttl_max;
+ struct timeval checkpoint_walltime;
+ uint64_t checkpoint_monotime;
+};
+
+typedef int32_t (*kr_stale_cb)(int32_t ttl, const knot_dname_t *owner, uint16_t type,
+ const struct kr_query *qry);
+
+void kr_rrset_init(knot_rrset_t *rrset, knot_dname_t *owner,
+ uint16_t type, uint16_t rclass, uint32_t ttl);
+struct kr_nsrep {
+ unsigned int score;
+ unsigned int reputation;
+ const knot_dname_t *name;
+ struct kr_context *ctx;
+ /* beware: hidden stub, to avoid hardcoding sockaddr lengths */
+};
+struct kr_query {
+ struct kr_query *parent;
+ knot_dname_t *sname;
+ uint16_t stype;
+ uint16_t sclass;
+ uint16_t id;
+ struct kr_qflags flags;
+ struct kr_qflags forward_flags;
+ uint32_t secret;
+ uint16_t fails;
+ uint16_t reorder;
+ uint64_t creation_time_mono;
+ uint64_t timestamp_mono;
+ struct timeval timestamp;
+ struct kr_zonecut zone_cut;
+ struct kr_layer_pickle *deferred;
+ uint32_t uid;
+ struct kr_query *cname_parent;
+ struct kr_request *request;
+ kr_stale_cb stale_cb;
+ struct kr_nsrep ns;
+};
+struct kr_context {
+ struct kr_qflags options;
+ knot_rrset_t *opt_rr;
+ map_t trust_anchors;
+ map_t negative_anchors;
+ struct kr_zonecut root_hints;
+ struct kr_cache cache;
+ char _stub[];
+};
+const char *knot_strerror(int);
+knot_dname_t *knot_dname_copy(const knot_dname_t *, knot_mm_t *);
+knot_dname_t *knot_dname_from_str(uint8_t *, const char *, size_t);
+int knot_dname_in_bailiwick(const knot_dname_t *, const knot_dname_t *);
+_Bool knot_dname_is_equal(const knot_dname_t *, const knot_dname_t *);
+size_t knot_dname_labels(const uint8_t *, const uint8_t *);
+size_t knot_dname_size(const knot_dname_t *);
+void knot_dname_to_lower(knot_dname_t *);
+char *knot_dname_to_str(char *, const knot_dname_t *, size_t);
+knot_rdata_t *knot_rdataset_at(const knot_rdataset_t *, uint16_t);
+int knot_rdataset_merge(knot_rdataset_t *, const knot_rdataset_t *, knot_mm_t *);
+int knot_rrset_add_rdata(knot_rrset_t *, const uint8_t *, uint16_t, knot_mm_t *);
+int knot_rrset_txt_dump(const knot_rrset_t *, char **, size_t *, const knot_dump_style_t *);
+int knot_rrset_txt_dump_data(const knot_rrset_t *, const size_t, char *, const size_t, const knot_dump_style_t *);
+size_t knot_rrset_size(const knot_rrset_t *);
+int knot_pkt_begin(knot_pkt_t *, knot_section_t);
+int knot_pkt_put_question(knot_pkt_t *, const knot_dname_t *, uint16_t, uint16_t);
+int knot_pkt_put_rotate(knot_pkt_t *, uint16_t, const knot_rrset_t *, uint16_t, uint16_t);
+knot_pkt_t *knot_pkt_new(void *, uint16_t, knot_mm_t *);
+void knot_pkt_free(knot_pkt_t *);
+int knot_pkt_parse(knot_pkt_t *, unsigned int);
+struct kr_rplan *kr_resolve_plan(struct kr_request *);
+knot_mm_t *kr_resolve_pool(struct kr_request *);
+struct kr_query *kr_rplan_push(struct kr_rplan *, struct kr_query *, const knot_dname_t *, uint16_t, uint16_t);
+int kr_rplan_pop(struct kr_rplan *, struct kr_query *);
+struct kr_query *kr_rplan_resolved(struct kr_rplan *);
+struct kr_query *kr_rplan_last(struct kr_rplan *);
+int kr_nsrep_set(struct kr_query *, size_t, const struct sockaddr *);
+int kr_make_query(struct kr_query *, knot_pkt_t *);
+void kr_pkt_make_auth_header(knot_pkt_t *);
+int kr_pkt_put(knot_pkt_t *, const knot_dname_t *, uint32_t, uint16_t, uint16_t, const uint8_t *, uint16_t);
+int kr_pkt_recycle(knot_pkt_t *);
+int kr_pkt_clear_payload(knot_pkt_t *);
+uint16_t kr_pkt_qclass(const knot_pkt_t *);
+uint16_t kr_pkt_qtype(const knot_pkt_t *);
+void kr_rnd_buffered(void *, unsigned int);
+uint32_t kr_rrsig_sig_inception(const knot_rdata_t *);
+uint32_t kr_rrsig_sig_expiration(const knot_rdata_t *);
+uint16_t kr_rrsig_type_covered(const knot_rdata_t *);
+const char *kr_inaddr(const struct sockaddr *);
+int kr_inaddr_family(const struct sockaddr *);
+int kr_inaddr_len(const struct sockaddr *);
+int kr_inaddr_str(const struct sockaddr *, char *, size_t *);
+int kr_sockaddr_len(const struct sockaddr *);
+uint16_t kr_inaddr_port(const struct sockaddr *);
+int kr_straddr_family(const char *);
+int kr_straddr_subnet(void *, const char *);
+int kr_bitcmp(const char *, const char *, int);
+int kr_family_len(int);
+struct sockaddr *kr_straddr_socket(const char *, int);
+int kr_ranked_rrarray_add(ranked_rr_array_t *, const knot_rrset_t *, uint8_t, _Bool, uint32_t, knot_mm_t *);
+void kr_qflags_set(struct kr_qflags *, struct kr_qflags);
+void kr_qflags_clear(struct kr_qflags *, struct kr_qflags);
+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 *);
+void *mm_realloc(knot_mm_t *, void *, size_t, size_t);
+knot_rrset_t *kr_ta_get(map_t *, const knot_dname_t *);
+int kr_ta_add(map_t *, const knot_dname_t *, uint16_t, uint32_t, const uint8_t *, uint16_t);
+int kr_ta_del(map_t *, const knot_dname_t *);
+void kr_ta_clear(map_t *);
+_Bool kr_dnssec_key_ksk(const uint8_t *);
+_Bool kr_dnssec_key_revoked(const uint8_t *);
+int kr_dnssec_key_tag(uint16_t, const uint8_t *, size_t);
+int kr_dnssec_key_match(const uint8_t *, size_t, const uint8_t *, size_t);
+int kr_cache_closest_apex(struct kr_cache *, const knot_dname_t *, _Bool, knot_dname_t **);
+int kr_cache_insert_rr(struct kr_cache *, const knot_rrset_t *, const knot_rrset_t *, uint8_t, uint32_t);
+int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t);
+int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int);
+int kr_cache_sync(struct kr_cache *);
+typedef struct {
+ uint8_t bitmap[32];
+ uint8_t length;
+} zs_win_t;
+typedef struct {
+ uint8_t excl_flag;
+ uint16_t addr_family;
+ uint8_t prefix_length;
+} zs_apl_t;
+typedef struct {
+ uint32_t d1;
+ uint32_t d2;
+ uint32_t m1;
+ uint32_t m2;
+ uint32_t s1;
+ uint32_t s2;
+ uint32_t alt;
+ uint64_t siz;
+ uint64_t hp;
+ uint64_t vp;
+ int8_t lat_sign;
+ int8_t long_sign;
+ int8_t alt_sign;
+} zs_loc_t;
+typedef enum {ZS_STATE_NONE, ZS_STATE_DATA, ZS_STATE_ERROR, ZS_STATE_INCLUDE, ZS_STATE_EOF, ZS_STATE_STOP} zs_state_t;
+typedef struct zs_scanner zs_scanner_t;
+struct zs_scanner {
+ int cs;
+ int top;
+ int stack[16];
+ _Bool multiline;
+ uint64_t number64;
+ uint64_t number64_tmp;
+ uint32_t decimals;
+ uint32_t decimal_counter;
+ uint32_t item_length;
+ uint32_t item_length_position;
+ uint8_t *item_length_location;
+ uint32_t buffer_length;
+ uint8_t buffer[65535];
+ char include_filename[65535];
+ char *path;
+ zs_win_t windows[256];
+ int16_t last_window;
+ zs_apl_t apl;
+ zs_loc_t loc;
+ uint8_t addr[16];
+ _Bool long_string;
+ uint8_t *dname;
+ uint32_t *dname_length;
+ uint32_t dname_tmp_length;
+ uint32_t r_data_tail;
+ uint32_t zone_origin_length;
+ uint8_t zone_origin[318];
+ uint16_t default_class;
+ uint32_t default_ttl;
+ zs_state_t state;
+ struct {
+ _Bool automatic;
+ void (*record)(zs_scanner_t *);
+ void (*error)(zs_scanner_t *);
+ void *data;
+ } process;
+ struct {
+ const char *start;
+ const char *current;
+ const char *end;
+ _Bool eof;
+ _Bool mmaped;
+ } input;
+ struct {
+ char *name;
+ int descriptor;
+ } file;
+ struct {
+ int code;
+ uint64_t counter;
+ _Bool fatal;
+ } error;
+ uint64_t line_counter;
+ uint32_t r_owner_length;
+ uint8_t r_owner[318];
+ uint16_t r_class;
+ uint32_t r_ttl;
+ uint16_t r_type;
+ uint32_t r_data_length;
+ uint8_t r_data[65535];
+};
+void zs_deinit(zs_scanner_t *);
+int zs_init(zs_scanner_t *, const char *, const uint16_t, const uint32_t);
+int zs_parse_record(zs_scanner_t *);
+int zs_set_input_file(zs_scanner_t *, const char *);
+int zs_set_input_string(zs_scanner_t *, const char *, size_t);
+const char *zs_strerror(const int);
+]]
diff --git a/daemon/lua/kres-gen.sh b/daemon/lua/kres-gen.sh
new file mode 100755
index 0000000..538fe23
--- /dev/null
+++ b/daemon/lua/kres-gen.sh
@@ -0,0 +1,216 @@
+#!/bin/bash
+set -o pipefail -o errexit
+
+### Dev's guide
+#
+# C declarations for lua are (mostly) generated to simplify maintenance.
+# (Avoid typos, accidental mismatches, etc.)
+#
+# To regenerate the C definitions for lua:
+# - you need to have debugging symbols for knot-dns and knot-resolver;
+# you get those by compiling with -g; for knot-dns it might be enough
+# to just install it with debugging symbols included (in your distro way)
+# - remove file ./kres-gen.lua and run make as usual
+# - the knot-dns libraries are found via pkg-config
+# - you also need gdb on $PATH
+
+
+printf -- "local ffi = require('ffi')\n"
+printf -- "--[[ This file is generated by ./kres-gen.sh ]] ffi.cdef[[\n"
+
+## Various types (mainly), from libknot and libkres
+
+printf "
+typedef struct knot_dump_style knot_dump_style_t;
+extern const knot_dump_style_t KNOT_DUMP_STYLE_DEFAULT;
+typedef void knot_db_t;
+struct kr_cdb_api {};
+struct lru {};
+"
+
+# The generator doesn't work well with typedefs of functions.
+printf "
+typedef struct knot_mm {
+ void *ctx, *alloc, *free;
+} knot_mm_t;
+
+typedef void *(*map_alloc_f)(void *, size_t);
+typedef void (*map_free_f)(void *baton, void *ptr);
+typedef void (*trace_log_f) (const struct kr_query *, const char *, const char *);
+typedef void (*trace_callback_f)(struct kr_request *);
+"
+
+./scripts/gen-cdefs.sh libkres types <<-EOF
+ knot_section_t
+ knot_rrinfo_t
+ knot_dname_t
+ knot_rdata_t
+ knot_rdataset_t
+EOF
+
+genResType() {
+ echo "$1" | ./scripts/gen-cdefs.sh libkres types
+}
+
+# No simple way to fixup this rename in ./kres.lua AFAIK.
+genResType "knot_rrset_t" | sed 's/\<owner\>/_owner/; s/\<ttl\>/_ttl/'
+
+./scripts/gen-cdefs.sh libkres types <<-EOF
+ knot_pkt_t
+ knot_edns_options_t
+ knot_pktsection_t
+ struct knot_compr
+ knot_compr_t
+ struct knot_pkt
+ # generics
+ map_t
+ # libkres
+ struct kr_qflags
+ rr_array_t
+ struct ranked_rr_array_entry
+ ranked_rr_array_entry_t
+ ranked_rr_array_t
+ trie_t
+ struct kr_zonecut
+ kr_qarray_t
+ struct kr_rplan
+ struct kr_request_qsource_flags
+ struct kr_request
+ enum kr_rank
+ struct kr_cache
+EOF
+
+printf "
+typedef int32_t (*kr_stale_cb)(int32_t ttl, const knot_dname_t *owner, uint16_t type,
+ const struct kr_query *qry);
+
+void kr_rrset_init(knot_rrset_t *rrset, knot_dname_t *owner,
+ uint16_t type, uint16_t rclass, uint32_t ttl);
+"
+
+## Some definitions would need too many deps, so shorten them.
+
+genResType "struct kr_nsrep" | sed '/union/,$ d'
+printf "\t/* beware: hidden stub, to avoid hardcoding sockaddr lengths */\n};\n"
+
+genResType "struct kr_query"
+
+genResType "struct kr_context" | sed '/kr_nsrep_rtt_lru_t/,$ d'
+printf "\tchar _stub[];\n};\n"
+
+## libknot API
+./scripts/gen-cdefs.sh libknot functions <<-EOF
+# Utils
+ knot_strerror
+# Domain names
+ knot_dname_copy
+ knot_dname_from_str
+ knot_dname_in_bailiwick
+ knot_dname_is_equal
+ knot_dname_labels
+ knot_dname_size
+ knot_dname_to_lower
+ knot_dname_to_str
+# Resource records
+ knot_rdataset_at
+ knot_rdataset_merge
+ knot_rrset_add_rdata
+ knot_rrset_txt_dump
+ knot_rrset_txt_dump_data
+ knot_rrset_size
+# Packet
+ knot_pkt_begin
+ knot_pkt_put_question
+ knot_pkt_put_rotate
+ knot_pkt_new
+ knot_pkt_free
+ knot_pkt_parse
+EOF
+
+## libkres API
+./scripts/gen-cdefs.sh libkres functions <<-EOF
+# Resolution request
+ kr_resolve_plan
+ kr_resolve_pool
+# Resolution plan
+ kr_rplan_push
+ kr_rplan_pop
+ kr_rplan_resolved
+ kr_rplan_last
+# Nameservers
+ kr_nsrep_set
+# Utils
+ kr_make_query
+ kr_pkt_make_auth_header
+ kr_pkt_put
+ kr_pkt_recycle
+ kr_pkt_clear_payload
+ kr_pkt_qclass
+ kr_pkt_qtype
+ kr_rnd_buffered
+ kr_rrsig_sig_inception
+ kr_rrsig_sig_expiration
+ kr_rrsig_type_covered
+ kr_inaddr
+ kr_inaddr_family
+ kr_inaddr_len
+ kr_inaddr_str
+ kr_sockaddr_len
+ kr_inaddr_port
+ kr_straddr_family
+ kr_straddr_subnet
+ kr_bitcmp
+ kr_family_len
+ kr_straddr_socket
+ kr_ranked_rrarray_add
+ kr_qflags_set
+ kr_qflags_clear
+ kr_zonecut_add
+ kr_zonecut_is_empty
+ kr_zonecut_set
+ kr_now
+ kr_strptime_diff
+ lru_free_items_impl
+ lru_create_impl
+ lru_get_impl
+ mm_realloc
+# Trust anchors
+ kr_ta_get
+ kr_ta_add
+ kr_ta_del
+ kr_ta_clear
+# DNSSEC
+ kr_dnssec_key_ksk
+ kr_dnssec_key_revoked
+ kr_dnssec_key_tag
+ kr_dnssec_key_match
+# Cache
+ kr_cache_closest_apex
+ kr_cache_insert_rr
+ kr_cache_remove
+ kr_cache_remove_subtree
+ kr_cache_sync
+EOF
+
+
+## libzscanner API for ./zonefile.lua
+./scripts/gen-cdefs.sh libzscanner types <<-EOF
+ zs_win_t
+ zs_apl_t
+ zs_loc_t
+ zs_state_t
+ zs_scanner_t
+ struct zs_scanner
+EOF
+./scripts/gen-cdefs.sh libzscanner functions <<-EOF
+ zs_deinit
+ zs_init
+ zs_parse_record
+ zs_set_input_file
+ zs_set_input_string
+ zs_strerror
+EOF
+
+printf "]]\n"
+
+exit 0
diff --git a/daemon/lua/kres.lua b/daemon/lua/kres.lua
new file mode 100644
index 0000000..8cb0046
--- /dev/null
+++ b/daemon/lua/kres.lua
@@ -0,0 +1,952 @@
+-- LuaJIT ffi bindings for libkres, a DNS resolver library.
+-- @note Since it's statically compiled, it expects to find the symbols in the C namespace.
+
+local kres -- the module
+
+local ffi = require('ffi')
+local bit = require('bit')
+local bor = bit.bor
+local band = bit.band
+local C = ffi.C
+local knot = ffi.load(libknot_SONAME)
+
+-- Inverse table
+local function itable(t, tolower)
+ local it = {}
+ for k,v in pairs(t) do it[v] = tolower and string.lower(k) or k end
+ return it
+end
+
+-- Byte order conversions
+local function htonl(x) return x end
+local htons = htonl
+if ffi.abi('le') then
+ htonl = bit.bswap
+ function htons(x) return bit.rshift(htonl(x), 16) end
+end
+
+-- Basic types
+local u16_p = ffi.typeof('uint16_t *')
+
+-- Various declarations that are very stable.
+ffi.cdef[[
+/*
+ * Data structures
+ */
+
+/* stdlib */
+typedef long time_t;
+struct timeval {
+ time_t tv_sec;
+ time_t tv_usec;
+};
+struct sockaddr {
+ uint16_t sa_family;
+ uint8_t _stub[]; /* Do not touch */
+};
+struct knot_error {
+ int code;
+};
+
+/*
+ * libc APIs
+ */
+void * malloc(size_t size);
+void free(void *ptr);
+int inet_pton(int af, const char *src, void *dst);
+int gettimeofday(struct timeval *tv, struct timezone *tz);
+]]
+
+require('kres-gen')
+
+-- Error code representation
+local knot_error_t = ffi.typeof('struct knot_error')
+ffi.metatype(knot_error_t, {
+ -- Convert libknot error strings
+ __tostring = function(self)
+ return ffi.string(knot.knot_strerror(self.code))
+ end,
+});
+
+-- Constant tables
+local const_class = {
+ IN = 1,
+ CH = 3,
+ NONE = 254,
+ ANY = 255,
+}
+local const_type = {
+ A = 1,
+ NS = 2,
+ MD = 3,
+ MF = 4,
+ CNAME = 5,
+ SOA = 6,
+ MB = 7,
+ MG = 8,
+ MR = 9,
+ NULL = 10,
+ WKS = 11,
+ PTR = 12,
+ HINFO = 13,
+ MINFO = 14,
+ MX = 15,
+ TXT = 16,
+ RP = 17,
+ AFSDB = 18,
+ X25 = 19,
+ ISDN = 20,
+ RT = 21,
+ NSAP = 22,
+ ['NSAP-PTR'] = 23,
+ SIG = 24,
+ KEY = 25,
+ PX = 26,
+ GPOS = 27,
+ AAAA = 28,
+ LOC = 29,
+ NXT = 30,
+ EID = 31,
+ NIMLOC = 32,
+ SRV = 33,
+ ATMA = 34,
+ NAPTR = 35,
+ KX = 36,
+ CERT = 37,
+ A6 = 38,
+ DNAME = 39,
+ SINK = 40,
+ OPT = 41,
+ APL = 42,
+ DS = 43,
+ SSHFP = 44,
+ IPSECKEY = 45,
+ RRSIG = 46,
+ NSEC = 47,
+ DNSKEY = 48,
+ DHCID = 49,
+ NSEC3 = 50,
+ NSEC3PARAM = 51,
+ TLSA = 52,
+ SMIMEA = 53,
+ HIP = 55,
+ NINFO = 56,
+ RKEY = 57,
+ TALINK = 58,
+ CDS = 59,
+ CDNSKEY = 60,
+ OPENPGPKEY = 61,
+ CSYNC = 62,
+ SPF = 99,
+ UINFO = 100,
+ UID = 101,
+ GID = 102,
+ UNSPEC = 103,
+ NID = 104,
+ L32 = 105,
+ L64 = 106,
+ LP = 107,
+ EUI48 = 108,
+ EUI64 = 109,
+ TKEY = 249,
+ TSIG = 250,
+ IXFR = 251,
+ AXFR = 252,
+ MAILB = 253,
+ MAILA = 254,
+ ANY = 255,
+ URI = 256,
+ CAA = 257,
+ AVC = 258,
+ DOA = 259,
+ TA = 32768,
+ DLV = 32769,
+}
+local const_section = {
+ ANSWER = 0,
+ AUTHORITY = 1,
+ ADDITIONAL = 2,
+}
+local const_opcode = {
+ QUERY = 0,
+ IQUERY = 1,
+ STATUS = 2,
+ NOTIFY = 4,
+ UPDATE = 5,
+}
+local const_rcode = {
+ NOERROR = 0,
+ FORMERR = 1,
+ SERVFAIL = 2,
+ NXDOMAIN = 3,
+ NOTIMPL = 4,
+ REFUSED = 5,
+ YXDOMAIN = 6,
+ YXRRSET = 7,
+ NXRRSET = 8,
+ NOTAUTH = 9,
+ NOTZONE = 10,
+ BADVERS = 16,
+ BADCOOKIE = 23,
+}
+-- This corresponds to `enum kr_rank`, it's not possible to do this without introspection unfortunately
+local const_rank = {
+ INITIAL = 0,
+ OMIT = 1,
+ TRY = 2,
+ INDET = 4,
+ BOGUS = 5,
+ MISMATCH = 6,
+ MISSING = 7,
+ INSECURE = 8,
+ AUTH = 16,
+ SECURE = 32
+}
+
+-- Constant tables
+local const_class_str = itable(const_class)
+local const_type_str = itable(const_type)
+local const_rcode_str = itable(const_rcode)
+local const_opcode_str = itable(const_opcode)
+local const_section_str = itable(const_section)
+local const_rank_str = itable(const_rank)
+
+-- Metatype for RR types to allow anonymous types
+setmetatable(const_type, {
+ __index = function (t, k)
+ local v = rawget(t, k)
+ if v then return v end
+ -- Allow TYPE%d notation
+ if string.find(k, 'TYPE', 1, true) then
+ return tonumber(k:sub(5))
+ end
+ -- Unknown type
+ return
+ end
+})
+
+-- Metatype for RR types to allow anonymous string types
+setmetatable(const_type_str, {
+ __index = function (t, k)
+ local v = rawget(t, k)
+ if v then return v end
+ return string.format('TYPE%d', k)
+ end
+})
+
+-- Metatype for timeval
+local timeval_t = ffi.typeof('struct timeval')
+
+-- Metatype for sockaddr
+local addr_buf = ffi.new('char[16]')
+local str_addr_buf = ffi.new('char[46 + 1 + 6 + 1]') -- IPv6 + #port + \0
+local str_addr_buf_len = ffi.sizeof(str_addr_buf)
+local sockaddr_t = ffi.typeof('struct sockaddr')
+ffi.metatype( sockaddr_t, {
+ __index = {
+ len = function(sa) return C.kr_inaddr_len(sa) end,
+ ip = function (sa) return C.kr_inaddr(sa) end,
+ family = function (sa) return C.kr_inaddr_family(sa) end,
+ port = function (sa) return C.kr_inaddr_port(sa) end,
+ },
+ __tostring = function(sa)
+ assert(ffi.istype(sockaddr_t, sa))
+ local len = ffi.new('size_t[1]', str_addr_buf_len)
+ local ret = C.kr_inaddr_str(sa, str_addr_buf, len)
+ if ret ~= 0 then
+ error('kr_inaddr_str failed: ' .. tostring(ret))
+ end
+ return ffi.string(str_addr_buf)
+ end,
+
+})
+
+-- Parametrized LRU table
+local typed_lru_t = 'struct { $ value_type[1]; struct lru * lru; }'
+
+-- Metatype for LRU
+local lru_metatype = {
+ -- Create a new LRU with given value type
+ -- By default the LRU will have a capacity of 65536 elements
+ -- Note: At the point the parametrized type must be finalized
+ __new = function (ct, max_slots)
+ -- {0} will make sure that the value is coercible to a number
+ local o = ffi.new(ct, {0}, C.lru_create_impl(max_slots or 65536, nil, nil))
+ if o.lru == nil then
+ return
+ end
+ return o
+ end,
+ -- Destructor to clean allocated memory
+ __gc = function (self)
+ assert(self.lru ~= nil)
+ C.lru_free_items_impl(self.lru)
+ C.free(self.lru)
+ self.lru = nil
+ end,
+ __index = {
+ -- Look up key and return reference to current
+ -- Note: The key will be inserted if it doesn't exist
+ get_ref = function (self, key, key_len, allow_insert)
+ local insert = allow_insert and true or false
+ local ptr = C.lru_get_impl(self.lru, key, key_len or #key, ffi.sizeof(self.value_type[0]), insert, nil)
+ if ptr ~= nil then
+ return ffi.cast(self.value_type, ptr)
+ end
+ end,
+ -- Look up key and return current value
+ get = function (self, key, key_len)
+ local ref = self:get_ref(key, key_len, false)
+ if ref then
+ return ref[0]
+ end
+ end,
+ -- Set value for key to given value
+ set = function (self, key, value, key_len)
+ local ref = self:get_ref(key, key_len, true)
+ if ref then
+ ref[0] = value
+ return true
+ end
+ end,
+ },
+}
+
+-- Pretty print for domain name
+local function dname2str(dname)
+ if dname == nil then return end
+ local text_name = ffi.gc(C.knot_dname_to_str(nil, dname, 0), C.free)
+ if text_name ~= nil then
+ return ffi.string(text_name)
+ end
+end
+
+-- Convert dname pointer to wireformat string
+local function dname2wire(name)
+ if name == nil then return nil end
+ return ffi.string(name, knot.knot_dname_size(name))
+end
+
+-- RR sets created in Lua must have a destructor to release allocated memory
+local function rrset_free(rr)
+ if rr._owner ~= nil then ffi.C.free(rr._owner) end
+ if rr:rdcount() > 0 then ffi.C.free(rr.rrs.rdata) end
+end
+
+-- Metatype for RR set. Beware, the indexing is 0-based (rdata, get, tostring).
+local rrset_buflen = (64 + 1) * 1024
+local rrset_buf = ffi.new('char[?]', rrset_buflen)
+local knot_rrset_pt = ffi.typeof('knot_rrset_t *')
+local knot_rrset_t = ffi.typeof('knot_rrset_t')
+ffi.metatype( knot_rrset_t, {
+ -- Create a new empty RR set object with an allocated owner and a destructor
+ __new = function (ct, owner, rrtype, rrclass, ttl)
+ local rr = ffi.new(ct)
+ C.kr_rrset_init(rr,
+ owner and knot.knot_dname_copy(owner, nil),
+ rrtype or 0,
+ rrclass or const_class.IN,
+ ttl or 0)
+ return ffi.gc(rr, rrset_free)
+ end,
+ -- BEWARE: `owner` and `rdata` are typed as a plain lua strings
+ -- and not the real types they represent.
+ __tostring = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return rr:txt_dump()
+ end,
+ __index = {
+ owner = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return dname2wire(rr._owner)
+ end,
+ ttl = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return tonumber(rr._ttl)
+ end,
+ class = function(rr, val)
+ assert(ffi.istype(knot_rrset_t, rr))
+ if val then
+ rr.rclass = val
+ end
+ return tonumber(rr.rclass)
+ end,
+ rdata_pt = function(rr, i)
+ assert(ffi.istype(knot_rrset_t, rr) and i >= 0 and i < rr:rdcount())
+ return knot.knot_rdataset_at(rr.rrs, i)
+ end,
+ rdata = function(rr, i)
+ assert(ffi.istype(knot_rrset_t, rr))
+ local rd = rr:rdata_pt(i)
+ return ffi.string(rd.data, rd.len)
+ end,
+ get = function(rr, i)
+ assert(ffi.istype(knot_rrset_t, rr) and i >= 0 and i < rr:rdcount())
+ return {owner = rr:owner(),
+ ttl = rr:ttl(),
+ class = tonumber(rr.rclass),
+ type = tonumber(rr.type),
+ rdata = rr:rdata(i)}
+ end,
+ tostring = function(rr, i)
+ assert(ffi.istype(knot_rrset_t, rr)
+ and (i == nil or (i >= 0 and i < rr:rdcount())) )
+ if rr:rdcount() > 0 then
+ local ret
+ if i ~= nil then
+ ret = knot.knot_rrset_txt_dump_data(rr, i, rrset_buf, rrset_buflen, knot.KNOT_DUMP_STYLE_DEFAULT)
+ else
+ ret = -1
+ end
+ return ret >= 0 and ffi.string(rrset_buf)
+ end
+ end,
+
+ -- Dump the rrset in presentation format (dig-like).
+ txt_dump = function(rr, style)
+ assert(ffi.istype(knot_rrset_t, rr))
+ local bufsize = 1024
+ local dump = ffi.new('char *[1]', C.malloc(bufsize))
+ -- ^ one pointer to a string
+ local size = ffi.new('size_t[1]', { bufsize }) -- one size_t = bufsize
+
+ local ret = knot.knot_rrset_txt_dump(rr, dump, size,
+ style or knot.KNOT_DUMP_STYLE_DEFAULT)
+ local result = nil
+ if ret >= 0 then
+ result = ffi.string(dump[0], ret)
+ end
+ C.free(dump[0])
+ return result
+ end,
+ -- Return RDATA count for this RR set
+ rdcount = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return tonumber(rr.rrs.count)
+ end,
+ -- Add binary RDATA to the RR set
+ add_rdata = function (rr, rdata, rdlen, no_ttl)
+ assert(ffi.istype(knot_rrset_t, rr))
+ assert(no_ttl == nil, 'add_rdata() can not accept TTL anymore')
+ local ret = knot.knot_rrset_add_rdata(rr, rdata, tonumber(rdlen), nil)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ -- Merge data from another RR set into the current one
+ merge_rdata = function (rr, source)
+ assert(ffi.istype(knot_rrset_t, rr))
+ assert(ffi.istype(knot_rrset_t, source))
+ local ret = knot.knot_rdataset_merge(rr.rrs, source.rrs, nil)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ -- Return type covered by this RRSIG
+ type_covered = function(rr, i)
+ i = i or 0
+ assert(ffi.istype(knot_rrset_t, rr) and i >= 0 and i < rr:rdcount())
+ if rr.type ~= const_type.RRSIG then return end
+ return tonumber(C.kr_rrsig_type_covered(knot.knot_rdataset_at(rr.rrs, i)))
+ end,
+ -- Check whether a RRSIG is covering current RR set
+ is_covered_by = function(rr, rrsig)
+ assert(ffi.istype(knot_rrset_t, rr))
+ assert(ffi.istype(knot_rrset_t, rrsig))
+ assert(rrsig.type == const_type.RRSIG)
+ return (rr.type == rrsig:type_covered() and rr:owner() == rrsig:owner())
+ end,
+ -- Return RR set wire size
+ wire_size = function(rr)
+ assert(ffi.istype(knot_rrset_t, rr))
+ return tonumber(knot.knot_rrset_size(rr))
+ end,
+ },
+})
+
+-- Destructor for packet accepts pointer to pointer
+local knot_pkt_t = ffi.typeof('knot_pkt_t')
+
+-- Helpers for reading/writing 16-bit numbers from packet wire
+local function pkt_u16(pkt, off, val)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ptr = ffi.cast(u16_p, pkt.wire + off)
+ if val ~= nil then ptr[0] = htons(val) end
+ return (htons(ptr[0]))
+end
+
+-- Helpers for reading/writing message header flags
+local function pkt_bit(pkt, byteoff, bitmask, val)
+ -- If the value argument is passed, set/clear the desired bit
+ if val ~= nil then
+ if val then pkt.wire[byteoff] = bit.bor(pkt.wire[byteoff], bitmask)
+ else pkt.wire[byteoff] = bit.band(pkt.wire[byteoff], bit.bnot(bitmask)) end
+ return true
+ end
+ return (bit.band(pkt.wire[byteoff], bitmask) ~= 0)
+end
+
+local function knot_pkt_rr(section, i)
+ assert(section and ffi.istype('knot_pktsection_t', section)
+ and i >= 0 and i < section.count)
+ local ret = section.pkt.rr + section.pos + i
+ assert(ffi.istype(knot_rrset_pt, ret))
+ return ret
+end
+
+-- Helpers for converting packet to text
+local function section_tostring(pkt, section_id)
+ local data = {}
+ local section = pkt.sections + section_id
+ if section.count > 0 then
+ table.insert(data, string.format('\n;; %s\n', const_section_str[section_id]))
+ for j = 0, section.count - 1 do
+ local rrset = knot_pkt_rr(section, j)
+ local rrtype = rrset.type
+ if rrtype ~= const_type.OPT and rrtype ~= const_type.TSIG then
+ table.insert(data, rrset:txt_dump())
+ end
+ end
+ end
+ return table.concat(data, '')
+end
+
+local function packet_tostring(pkt)
+ local hdr = string.format(';; ->>HEADER<<- opcode: %s; status: %s; id: %d\n',
+ const_opcode_str[pkt:opcode()], const_rcode_str[pkt:rcode()], pkt:id())
+ local flags = {}
+ for _,v in ipairs({'rd', 'tc', 'aa', 'qr', 'cd', 'ad', 'ra'}) do
+ if(pkt[v](pkt)) then table.insert(flags, v) end
+ end
+ local info = string.format(';; Flags: %s; QUERY: %d; ANSWER: %d; AUTHORITY: %d; ADDITIONAL: %d\n',
+ table.concat(flags, ' '), pkt:qdcount(), pkt:ancount(), pkt:nscount(), pkt:arcount())
+ local data = '\n'
+ if pkt.opt_rr ~= nil then
+ data = data..string.format(';; OPT PSEUDOSECTION:\n%s', pkt.opt_rr:tostring())
+ end
+ if pkt.tsig_rr ~= nil then
+ data = data..string.format(';; TSIG PSEUDOSECTION:\n%s', pkt.tsig_rr:tostring())
+ end
+ -- Zone transfer answers may omit question
+ if pkt:qdcount() > 0 then
+ data = data..string.format(';; QUESTION\n;; %s\t%s\t%s\n',
+ dname2str(pkt:qname()), const_type_str[pkt:qtype()], const_class_str[pkt:qclass()])
+ end
+ local data_sec = {}
+ for i = const_section.ANSWER, const_section.ADDITIONAL do
+ table.insert(data_sec, section_tostring(pkt, i))
+ end
+ return hdr..info..data..table.concat(data_sec, '')
+end
+
+-- Metatype for packet
+ffi.metatype( knot_pkt_t, {
+ __new = function (_, size, wire)
+ if size < 12 or size > 65535 then
+ error('packet size must be <12, 65535>')
+ end
+
+ local pkt = knot.knot_pkt_new(nil, size, nil)
+ if pkt == nil then
+ error(string.format('failed to allocate a packet of size %d', size))
+ end
+ if wire == nil then
+ C.kr_rnd_buffered(pkt.wire, 2) -- randomize the query ID
+ else
+ assert(size <= #wire)
+ ffi.copy(pkt.wire, wire, size)
+ pkt.size = size
+ pkt.parsed = 0
+ end
+
+ return ffi.gc(pkt[0], knot.knot_pkt_free)
+ end,
+ __tostring = function(pkt)
+ return pkt:tostring()
+ end,
+ __len = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return tonumber(pkt.size)
+ end,
+ __ipairs = function(self)
+ return ipairs(self:section(const_section.ANSWER))
+ end,
+ __index = {
+ -- Header
+ id = function(pkt, val) return pkt_u16(pkt, 0, val) end,
+ qdcount = function(pkt, val) return pkt_u16(pkt, 4, val) end,
+ ancount = function(pkt, val) return pkt_u16(pkt, 6, val) end,
+ nscount = function(pkt, val) return pkt_u16(pkt, 8, val) end,
+ arcount = function(pkt, val) return pkt_u16(pkt, 10, val) end,
+ opcode = function (pkt, val)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ pkt.wire[2] = (val) and bit.bor(bit.band(pkt.wire[2], 0x78), 8 * val) or pkt.wire[2]
+ return (bit.band(pkt.wire[2], 0x78) / 8)
+ end,
+ rcode = function (pkt, val)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ pkt.wire[3] = (val) and bor(band(pkt.wire[3], 0xf0), val) or pkt.wire[3]
+ return band(pkt.wire[3], 0x0f)
+ end,
+ rd = function (pkt, val) return pkt_bit(pkt, 2, 0x01, val) end,
+ tc = function (pkt, val) return pkt_bit(pkt, 2, 0x02, val) end,
+ aa = function (pkt, val) return pkt_bit(pkt, 2, 0x04, val) end,
+ qr = function (pkt, val) return pkt_bit(pkt, 2, 0x80, val) end,
+ cd = function (pkt, val) return pkt_bit(pkt, 3, 0x10, val) end,
+ ad = function (pkt, val) return pkt_bit(pkt, 3, 0x20, val) end,
+ ra = function (pkt, val) return pkt_bit(pkt, 3, 0x80, val) end,
+ -- Question
+ qname = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ -- inlined knot_pkt_qname(), basically
+ if pkt == nil or pkt.qname_size == 0 then return nil end
+ return ffi.string(pkt.wire + 12, pkt.qname_size)
+ end,
+ qclass = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return C.kr_pkt_qclass(pkt)
+ end,
+ qtype = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return C.kr_pkt_qtype(pkt)
+ end,
+ rrsets = function (pkt, section_id)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local records = {}
+ local section = pkt.sections + section_id
+ for i = 1, section.count do
+ local rrset = knot_pkt_rr(section, i - 1)
+ table.insert(records, rrset)
+ end
+ return records
+ end,
+ section = function (pkt, section_id)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local records = {}
+ local section = pkt.sections + section_id
+ for i = 1, section.count do
+ local rrset = knot_pkt_rr(section, i - 1)
+ for k = 1, rrset:rdcount() do
+ table.insert(records, rrset:get(k - 1))
+ end
+ end
+ return records
+ end,
+ begin = function (pkt, section)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ assert(section >= pkt.current, 'cannot rewind to already written section')
+ assert(const_section_str[section], string.format('invalid section: %s', section))
+ local ret = knot.knot_pkt_begin(pkt, section)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ put = function (pkt, owner, ttl, rclass, rtype, rdata)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = C.kr_pkt_put(pkt, owner, ttl, rclass, rtype, rdata, #rdata)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ -- Put an RR set in the packet
+ -- Note: the packet doesn't take ownership of the RR set
+ put_rr = function (pkt, rr, rotate, flags)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ assert(ffi.istype(knot_rrset_t, rr))
+ local ret = C.knot_pkt_put_rotate(pkt, 0, rr, rotate or 0, flags or 0)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ recycle = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = C.kr_pkt_recycle(pkt)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ clear_payload = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = C.kr_pkt_clear_payload(pkt)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ question = function(pkt, qname, qclass, qtype)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ assert(qclass ~= nil, string.format('invalid class: %s', qclass))
+ assert(qtype ~= nil, string.format('invalid type: %s', qtype))
+ local ret = C.knot_pkt_put_question(pkt, qname, qclass, qtype)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ towire = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return ffi.string(pkt.wire, pkt.size)
+ end,
+ tostring = function(pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ return packet_tostring(pkt)
+ end,
+ -- Return number of remaining empty bytes in the packet
+ -- This is generally useful to check if there's enough space
+ remaining_bytes = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local occupied = pkt.size + pkt.reserved
+ assert(pkt.max_size >= occupied)
+ return tonumber(pkt.max_size - occupied)
+ end,
+ -- Packet manipulation
+ parse = function (pkt)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = knot.knot_pkt_parse(pkt, 0)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ -- Resize packet wire to a new size
+ resize = function (pkt, new_size)
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ptr = C.mm_realloc(pkt.mm, pkt.wire, new_size, pkt.max_size)
+ if ptr == nil then return end
+ pkt.wire = ptr
+ pkt.max_size = new_size
+ return true
+ end,
+ },
+})
+-- Metatype for query
+local kr_query_t = ffi.typeof('struct kr_query')
+ffi.metatype( kr_query_t, {
+ __index = {
+ -- Return query domain name
+ name = function(qry)
+ assert(ffi.istype(kr_query_t, qry))
+ return dname2wire(qry.sname)
+ end,
+ -- Write this query into packet
+ write = function(qry, pkt)
+ assert(ffi.istype(kr_query_t, qry))
+ assert(ffi.istype(knot_pkt_t, pkt))
+ local ret = C.kr_make_query(qry, pkt)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ },
+})
+-- Metatype for request
+local kr_request_t = ffi.typeof('struct kr_request')
+ffi.metatype( kr_request_t, {
+ __index = {
+ current = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ if req.current_query == nil then return nil end
+ return req.current_query
+ end,
+ -- Return last query on the resolution plan
+ last = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ local query = C.kr_rplan_last(C.kr_resolve_plan(req))
+ if query == nil then return end
+ return query
+ end,
+ resolved = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ local qry = C.kr_rplan_resolved(C.kr_resolve_plan(req))
+ if qry == nil then return nil end
+ return qry
+ end,
+ -- returns first resolved sub query for a request
+ first_resolved = function(req)
+ assert(ffi.istype(kr_request_t, req))
+ local rplan = C.kr_resolve_plan(req)
+ if not rplan or rplan.resolved.len < 1 then return nil end
+ return rplan.resolved.at[0]
+ end,
+ push = function(req, qname, qtype, qclass, flags, parent)
+ assert(ffi.istype(kr_request_t, req))
+ flags = kres.mk_qflags(flags) -- compatibility
+ local rplan = C.kr_resolve_plan(req)
+ local qry = C.kr_rplan_push(rplan, parent, qname, qclass, qtype)
+ if qry ~= nil and flags ~= nil then
+ C.kr_qflags_set(qry.flags, flags)
+ end
+ return qry
+ end,
+ pop = function(req, qry)
+ assert(ffi.istype(kr_request_t, req))
+ return C.kr_rplan_pop(C.kr_resolve_plan(req), qry)
+ end,
+ -- Return per-request variable table
+ -- The request can store anything in this Lua table and it will be freed
+ -- when the request is closed, it doesn't have to worry about contents.
+ vars = function (req)
+ assert(ffi.istype(kr_request_t, req))
+ -- Return variable if it's already stored
+ local var = worker.vars[req.vars_ref]
+ if var then
+ return var
+ end
+ -- Either take a slot number from freelist
+ -- or find a first free slot (expand the table)
+ local ref = worker.vars[0]
+ if ref then
+ worker.vars[0] = worker.vars[ref]
+ else
+ ref = #worker.vars + 1
+ end
+ -- Create new variables table
+ var = {}
+ worker.vars[ref] = var
+ -- Save reference in the request
+ req.vars_ref = ref
+ return var
+ end,
+ },
+})
+
+-- C array iterator
+local function c_array_iter(t, i)
+ i = i + 1
+ if i >= t.len then return end
+ return i, t.at[i][0]
+end
+
+-- Metatype for ranked record array
+local ranked_rr_array_t = ffi.typeof('ranked_rr_array_t')
+ffi.metatype(ranked_rr_array_t, {
+ __len = function(self)
+ return tonumber(self.len)
+ end,
+ __ipairs = function (self)
+ return c_array_iter, self, -1
+ end,
+ __index = {
+ get = function (self, i)
+ if i < 0 or i > self.len then return nil end
+ return self.at[i][0]
+ end,
+ }
+})
+
+-- Cache metatype
+local kr_cache_t = ffi.typeof('struct kr_cache')
+ffi.metatype( kr_cache_t, {
+ __index = {
+ insert = function (self, rr, rrsig, rank, timestamp)
+ assert(ffi.istype(kr_cache_t, self))
+ assert(ffi.istype(knot_rrset_t, rr), 'RR must be a rrset type')
+ assert(not rrsig or ffi.istype(knot_rrset_t, rrsig), 'RRSIG must be nil or of the rrset type')
+ -- Get current timestamp
+ if not timestamp then
+ local now = timeval_t()
+ C.gettimeofday(now, nil)
+ timestamp = tonumber(now.tv_sec)
+ end
+ -- Insert record into cache
+ local ret = C.kr_cache_insert_rr(self, rr, rrsig, tonumber(rank or 0), timestamp)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ sync = function (self)
+ assert(ffi.istype(kr_cache_t, self))
+ local ret = C.kr_cache_sync(self)
+ if ret ~= 0 then return nil, knot_error_t(ret) end
+ return true
+ end,
+ },
+})
+
+-- Pretty-print a single RR (which is a table with .owner .ttl .type .rdata)
+-- Extension: append .comment if exists.
+local function rr2str(rr, style)
+ -- Construct a single-RR temporary set while minimizing copying.
+ local ret
+ do
+ local rrs = knot_rrset_t(rr.owner, rr.type, kres.class.IN, rr.ttl)
+ rrs:add_rdata(rr.rdata, #rr.rdata)
+ ret = rrs:txt_dump(style)
+ end
+
+ -- Trim the newline and append comment (optionally).
+ if ret then
+ if ret:byte(-1) == string.byte('\n', -1) then
+ ret = ret:sub(1, -2)
+ end
+ if rr.comment then
+ ret = ret .. ' ;' .. rr.comment
+ end
+ end
+ return ret
+end
+
+-- Module API
+kres = {
+ -- Constants
+ class = const_class,
+ type = const_type,
+ section = const_section,
+ rcode = const_rcode,
+ opcode = const_opcode,
+ rank = const_rank,
+
+ -- Constants to strings
+ tostring = {
+ class = const_class_str,
+ type = const_type_str,
+ section = const_section_str,
+ rcode = const_rcode_str,
+ opcode = const_opcode_str,
+ rank = const_rank_str,
+ },
+
+ -- Create a struct kr_qflags from a single flag name or a list of names.
+ mk_qflags = function (names)
+ local kr_qflags = ffi.typeof('struct kr_qflags')
+ if names == 0 or names == nil then -- compatibility: nil is common in lua
+ names = {}
+ elseif type(names) == 'string' then
+ names = {names}
+ elseif ffi.istype(kr_qflags, names) then
+ return names
+ end
+
+ local fs = ffi.new(kr_qflags)
+ for _, name in pairs(names) do
+ fs[name] = true
+ end
+ return fs
+ end,
+
+ CONSUME = 1, PRODUCE = 2, DONE = 4, FAIL = 8, YIELD = 16,
+
+ -- Export types
+ rrset = knot_rrset_t,
+ packet = knot_pkt_t,
+ lru = function (max_size, value_type)
+ local ct = ffi.typeof(typed_lru_t, value_type or ffi.typeof('uint64_t'))
+ return ffi.metatype(ct, lru_metatype)(max_size)
+ end,
+
+ -- Metatypes. Beware that any pointer will be cast silently...
+ pkt_t = function (udata) return ffi.cast('knot_pkt_t *', udata) end,
+ request_t = function (udata) return ffi.cast('struct kr_request *', udata) end,
+ sockaddr_t = function (udata) return ffi.cast('struct sockaddr *', udata) end,
+
+ -- Global API functions
+ -- Convert a lua string to a lower-case wire format (inside GC-ed ffi.string).
+ str2dname = function(name)
+ if type(name) ~= 'string' then return end
+ local dname = ffi.gc(C.knot_dname_from_str(nil, name, 0), C.free)
+ if dname == nil then return nil end
+ ffi.C.knot_dname_to_lower(dname);
+ return dname2wire(dname)
+ end,
+ dname2str = dname2str,
+ dname2wire = dname2wire,
+
+ rr2str = rr2str,
+ str2ip = function (ip)
+ local family = C.kr_straddr_family(ip)
+ local ret = C.inet_pton(family, ip, addr_buf)
+ if ret ~= 1 then return nil end
+ return ffi.string(addr_buf, C.kr_family_len(family))
+ end,
+ context = function () return ffi.cast('struct kr_context *', __engine) end,
+
+ knot_pkt_rr = knot_pkt_rr,
+}
+
+return kres
diff --git a/daemon/lua/sandbox.lua b/daemon/lua/sandbox.lua
new file mode 100644
index 0000000..017e3a3
--- /dev/null
+++ b/daemon/lua/sandbox.lua
@@ -0,0 +1,458 @@
+local ffi = require('ffi')
+
+-- Units
+kB = 1024
+MB = 1024*kB
+GB = 1024*MB
+-- Time
+sec = 1000
+second = sec
+minute = 60 * sec
+min = minute
+hour = 60 * minute
+day = 24 * hour
+
+-- Logging
+function panic(fmt, ...)
+ error(string.format('error: '..fmt, ...))
+end
+function warn(fmt, ...)
+ io.stderr:write(string.format(fmt..'\n', ...))
+end
+function log(fmt, ...)
+ print(string.format(fmt, ...))
+end
+
+-- Resolver bindings
+kres = require('kres')
+if rawget(kres, 'str2dname') ~= nil then
+ todname = kres.str2dname
+end
+
+-- Compatibility wrapper for query flags.
+worker.resolve = function (qname, qtype, qclass, options, finish, init)
+ -- Alternatively use named arguments
+ if type(qname) == 'table' then
+ local t = qname
+ qname = t.name
+ qtype = t.type or kres.type.A
+ qclass = t.class or kres.class.IN
+ options = t.options
+ finish = t.finish
+ init = t.init
+ end
+
+ local init_cb, finish_cb = init, nil
+ if finish then
+ -- Create callback for finalization
+ finish_cb = ffi.cast('trace_callback_f', function (req)
+ req = kres.request_t(req)
+ finish(req.answer, req)
+ finish_cb:free()
+ end)
+ -- Wrap initialiser to install finish callback
+ init_cb = function (req)
+ req = kres.request_t(req)
+ if init then init(req) end
+ req.trace_finish = finish_cb
+ end
+ end
+
+ -- Translate options and resolve
+ options = kres.mk_qflags(options)
+ return worker.resolve_unwrapped(qname, qtype, qclass, options, init_cb)
+end
+
+resolve = worker.resolve
+
+-- Shorthand for aggregated per-worker information
+worker.info = function ()
+ local t = worker.stats()
+ t.pid = worker.pid
+ return t
+end
+
+-- Resolver mode of operation
+local current_mode = 'normal'
+local mode_table = { normal=0, strict=1, permissive=2 }
+function mode(m)
+ if not m then return current_mode end
+ if not mode_table[m] then error('unsupported mode: '..m) end
+ -- Update current operation mode
+ current_mode = m
+ option('STRICT', current_mode == 'strict')
+ option('PERMISSIVE', current_mode == 'permissive')
+ return true
+end
+
+-- Trivial option alias
+function reorder_RR(val)
+ return option('REORDER_RR', val)
+end
+
+-- Get/set resolver options via name (string)
+function option(name, val)
+ local flags = kres.context().options;
+ -- Note: no way to test existence of flags[name] but we want error anyway.
+ name = string.upper(name) -- convenience
+ if val ~= nil then
+ if (val ~= true) and (val ~= false) then
+ panic('invalid option value: ' .. tostring(val))
+ end
+ flags[name] = val;
+ end
+ return flags[name];
+end
+
+-- Function aliases
+-- `env.VAR returns os.getenv(VAR)`
+env = {}
+setmetatable(env, {
+ __index = function (_, k) return os.getenv(k) end
+})
+
+-- Quick access to interfaces
+-- `net.<iface>` => `net.interfaces()[iface]`
+-- `net = {addr1, ..}` => `net.listen(name, addr1)`
+-- `net.ipv{4,6} = {true, false}` => enable/disable IPv{4,6}
+setmetatable(net, {
+ __index = function (t, k)
+ local v = rawget(t, k)
+ if v then return v
+ elseif k == 'ipv6' then return not option('NO_IPV6')
+ elseif k == 'ipv4' then return not option('NO_IPV4')
+ else return net.interfaces()[k]
+ end
+ end,
+ __newindex = function (t,k,v)
+ if k == 'ipv6' then return option('NO_IPV6', not v)
+ elseif k == 'ipv4' then return option('NO_IPV4', not v)
+ else
+ local iname = rawget(net.interfaces(), v)
+ if iname then t.listen(iname)
+ else t.listen(v)
+ end
+ end
+ end
+})
+
+-- Syntactic sugar for module loading
+-- `modules.<name> = <config>`
+setmetatable(modules, {
+ __newindex = function (_, k, v)
+ if type(k) == 'number' then
+ k, v = v, nil
+ end
+ if not rawget(_G, k) then
+ modules.load(k)
+ k = string.match(k, '[%w_]+')
+ local mod = _G[k]
+ local config = mod and rawget(mod, 'config')
+ if mod ~= nil and config ~= nil then
+ if k ~= v then config(v)
+ else config()
+ end
+ end
+ end
+ end
+})
+
+
+cache.clear = function (name, exact_name, rr_type, chunk_size, callback, prev_state)
+ if name == nil or (name == '.' and not exact_name) then
+ -- keep same output format as for 'standard' clear
+ local total_count = cache.count()
+ if not cache.clear_everything() then
+ error('unable to clear everything')
+ end
+ return {count = total_count}
+ end
+ -- Check parameters, in order, and set defaults if missing.
+ local dname = kres.str2dname(name)
+ if not dname then error('cache.clear(): incorrect name passed') end
+ if exact_name == nil then exact_name = false end
+ if type(exact_name) ~= 'boolean'
+ then error('cache.clear(): incorrect exact_name passed') end
+
+ local cach = kres.context().cache;
+ local rettable = {}
+ -- Apex warning. If the caller passes a custom callback,
+ -- we assume they are advanced enough not to need the check.
+ -- The point is to avoid repeating the check in each callback iteration.
+ if callback == nil then
+ local apex_array = ffi.new('knot_dname_t *[1]') -- C: dname **apex_array
+ local ret = ffi.C.kr_cache_closest_apex(cach, dname, false, apex_array)
+ if ret < 0 then
+ error(ffi.string(ffi.C.knot_strerror(ret))) end
+ if not ffi.C.knot_dname_is_equal(apex_array[0], dname) then
+ local apex_str = kres.dname2str(apex_array[0])
+ rettable.not_apex = 'to clear proofs of non-existence call '
+ .. 'cache.clear(\'' .. tostring(apex_str) ..'\')'
+ rettable.subtree = apex_str
+ end
+ ffi.C.free(apex_array[0])
+ end
+
+ if rr_type ~= nil then
+ -- Special case, without any subtree searching.
+ if not exact_name
+ then error('cache.clear(): specifying rr_type only supported with exact_name') end
+ if chunk_size or callback
+ then error('cache.clear(): chunk_size and callback parameters not supported with rr_type') end
+ local ret = ffi.C.kr_cache_remove(cach, dname, rr_type)
+ if ret < 0 then error(ffi.string(ffi.C.knot_strerror(ret))) end
+ return {count = 1}
+ end
+
+ if chunk_size == nil then chunk_size = 100 end
+ if type(chunk_size) ~= 'number' or chunk_size <= 0
+ then error('cache.clear(): chunk_size has to be a positive integer') end
+
+ -- Do the C call, and add chunk_size warning.
+ rettable.count = ffi.C.kr_cache_remove_subtree(cach, dname, exact_name, chunk_size)
+ if rettable.count == chunk_size then
+ local msg_extra = ''
+ if callback == nil then
+ msg_extra = '; the default callback will continue asynchronously'
+ end
+ rettable.chunk_limit = 'chunk size limit reached' .. msg_extra
+ end
+
+ -- Default callback function: repeat after 1ms
+ if callback == nil then callback =
+ function (cbname, cbexact_name, cbrr_type, cbchunk_size, cbself, cbprev_state, cbrettable)
+ if cbrettable.count < 0 then error(ffi.string(ffi.C.knot_strerror(cbrettable.count))) end
+ if cbprev_state == nil then cbprev_state = { round = 0 } end
+ if type(cbprev_state) ~= 'table'
+ then error('cache.clear() callback: incorrect prev_state passed') end
+ cbrettable.round = cbprev_state.round + 1
+ if (cbrettable.count == cbchunk_size) then
+ event.after(1, function ()
+ cache.clear(cbname, cbexact_name, cbrr_type, cbchunk_size, cbself, cbrettable)
+ end)
+ elseif cbrettable.round > 1 then
+ log('[cache] asynchonous cache.clear(\'' .. cbname .. '\', '
+ .. tostring(cbexact_name) .. ') finished')
+ end
+ return cbrettable
+ end
+ end
+ return callback(name, exact_name, rr_type, chunk_size, callback, prev_state, rettable)
+end
+-- Syntactic sugar for cache
+-- `cache[x] -> cache.get(x)`
+-- `cache.{size|storage} = value`
+setmetatable(cache, {
+ __index = function (t, k)
+ local res = rawget(t, k)
+ if not res and not rawget(t, 'current_size') then return res end
+ -- Beware: t.get returns empty table on failure to find.
+ -- That would be confusing here (breaking kresc), so return nil instead.
+ res = t.get(k)
+ if res and next(res) ~= nil then return res else return nil end
+ end,
+ __newindex = function (t,k,v)
+ -- Defaults
+ local storage = rawget(t, 'current_storage')
+ if not storage then storage = 'lmdb://' end
+ local size = rawget(t, 'current_size')
+ if not size then size = 10*MB end
+ -- Declarative interface for cache
+ if k == 'size' then t.open(v, storage)
+ elseif k == 'storage' then t.open(size, v) end
+ end
+})
+
+-- Register module in Lua environment
+function modules_register(module)
+ -- Syntactic sugar for get() and set() properties
+ setmetatable(module, {
+ __index = function (t, k)
+ local v = rawget(t, k)
+ if v then return v
+ elseif rawget(t, 'get') then return t.get(k)
+ end
+ end,
+ __newindex = function (t, k, v)
+ local old_v = rawget(t, k)
+ if not old_v and rawget(t, 'set') then
+ t.set(k..' '..v)
+ end
+ end
+ })
+end
+
+-- Make sandboxed environment
+local function make_sandbox(defined)
+ local __protected = { worker = true, env = true, modules = true, cache = true, net = true, trust_anchors = true }
+
+ -- Compute and export the list of top-level names (hidden otherwise)
+ local nl = ""
+ for n in pairs(defined) do
+ nl = nl .. n .. "\n"
+ end
+
+ return setmetatable({ __orig_name_list = nl }, {
+ __index = defined,
+ __newindex = function (_, k, v)
+ if __protected[k] then
+ for k2,v2 in pairs(v) do
+ defined[k][k2] = v2
+ end
+ else
+ defined[k] = v
+ end
+ end
+ })
+end
+
+-- Compatibility sandbox
+if setfenv then -- Lua 5.1 and less
+ _G = make_sandbox(getfenv(0))
+ setfenv(0, _G)
+else -- Lua 5.2+
+ _SANDBOX = make_sandbox(_ENV)
+end
+
+-- Load embedded modules
+trust_anchors = require('trust_anchors')
+modules.load('ta_signal_query')
+modules.load('policy')
+modules.load('priming')
+modules.load('detect_time_skew')
+modules.load('detect_time_jump')
+modules.load('ta_sentinel')
+modules.load('edns_keepalive')
+
+-- Interactive command evaluation
+function eval_cmd(line, raw)
+ -- Compatibility sandbox code loading
+ local function load_code(code)
+ if getfenv then -- Lua 5.1
+ return loadstring(code)
+ else -- Lua 5.2+
+ return load(code, nil, 't', _ENV)
+ end
+ end
+ local err, chunk
+ chunk, err = load_code(raw and 'return '..line or 'return table_print('..line..')')
+ if err then
+ chunk, err = load_code(line)
+ end
+ if not err then
+ return chunk()
+ else
+ error(err)
+ end
+end
+
+-- Pretty printing
+function table_print (tt, indent, done)
+ done = done or {}
+ indent = indent or 0
+ local result = ""
+ -- Convert to printable string (escape unprintable)
+ local function printable(value)
+ value = tostring(value)
+ local bytes = {}
+ for i = 1, #value do
+ local c = string.byte(value, i)
+ if c >= 0x20 and c < 0x7f then table.insert(bytes, string.char(c))
+ else table.insert(bytes, '\\'..tostring(c))
+ end
+ if i > 80 then table.insert(bytes, '...') break end
+ end
+ return table.concat(bytes)
+ end
+ if type(tt) == "table" then
+ for key, value in pairs (tt) do
+ result = result .. string.rep (" ", indent)
+ if type (value) == "table" and not done [value] then
+ done [value] = true
+ result = result .. string.format("[%s] => {\n", printable (key))
+ result = result .. table_print (value, indent + 4, done)
+ result = result .. string.rep (" ", indent)
+ result = result .. "}\n"
+ else
+ result = result .. string.format("[%s] => %s\n",
+ tostring (key), printable(value))
+ end
+ end
+ else
+ result = result .. tostring(tt) .. "\n"
+ end
+ return result
+end
+
+-- This extends the worker module to allow asynchronous execution of functions and nonblocking I/O.
+-- The current implementation combines cqueues for Lua interface, and event.socket() in order to not
+-- block resolver engine while waiting for I/O or timers.
+--
+local has_cqueues, cqueues = pcall(require, 'cqueues')
+if has_cqueues then
+
+ -- Export the asynchronous sleep function
+ worker.sleep = cqueues.sleep
+
+ -- Create metatable for workers to define the API
+ -- It can schedule multiple cqueues and yield execution when there's a wait for blocking I/O or timer
+ local asynchronous_worker_mt = {
+ work = function (self)
+ local ok, err, _, co = self.cq:step(0)
+ if not ok then
+ warn('[%s] error: %s %s', self.name or 'worker', err, debug.traceback(co))
+ end
+ -- Reschedule timeout or create new one
+ local timeout = self.cq:timeout()
+ if timeout then
+ -- Throttle timeouts to avoid too frequent wakeups
+ if timeout == 0 then timeout = 0.00001 end
+ -- Convert from seconds to duration
+ timeout = timeout * sec
+ if not self.next_timeout then
+ self.next_timeout = event.after(timeout, self.on_step)
+ else
+ event.reschedule(self.next_timeout, timeout)
+ end
+ else -- Cancel running timeout when there is no next deadline
+ if self.next_timeout then
+ event.cancel(self.next_timeout)
+ self.next_timeout = nil
+ end
+ end
+ end,
+ wrap = function (self, f)
+ self.cq:wrap(f)
+ end,
+ loop = function (self)
+ self.on_step = function () self:work() end
+ self.event_fd = event.socket(self.cq:pollfd(), self.on_step)
+ end,
+ close = function (self)
+ if self.event_fd then
+ event.cancel(self.event_fd)
+ self.event_fd = nil
+ end
+ end,
+ }
+
+ -- Implement the coroutine worker with cqueues
+ local function worker_new (name)
+ return setmetatable({name = name, cq = cqueues.new()}, { __index = asynchronous_worker_mt })
+ end
+
+ -- Create a default background worker
+ worker.bg_worker = worker_new('worker.background')
+ worker.bg_worker:loop()
+
+ -- Wrap a function for asynchronous execution
+ function worker.coroutine (f)
+ worker.bg_worker:wrap(f)
+ end
+else
+ -- Disable asynchronous execution
+ local function disabled () error('cqueues are required for asynchronous execution') end
+ worker.sleep = disabled
+ worker.map = disabled
+ worker.coroutine = disabled
+end
diff --git a/daemon/lua/trust_anchors.lua.in b/daemon/lua/trust_anchors.lua.in
new file mode 100644
index 0000000..1d16018
--- /dev/null
+++ b/daemon/lua/trust_anchors.lua.in
@@ -0,0 +1,663 @@
+-- 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
+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 = {}
+ local r, c = 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
+
+-- 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)
+ -- RFC 7958, sec. 2, but we don't do precise XML parsing.
+ -- @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
+
+ -- 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.
+ local keydigests = {}
+ string.gsub(xml, "<KeyDigest([^>]*)>(.-)</KeyDigest>", function(attrs, inside)
+ parse_xml_keydigest(attrs, inside, keydigests)
+ end)
+ 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:'
+ .. rrset
+ return rrset, msg
+end
+
+-- 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)
+ 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
+ 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
+ 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, force_valid)
+ if rr.type == kres.type.DNSKEY and not C.kr_dnssec_key_ksk(rr.rdata) then
+ return false -- Ignore
+ 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 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)
+ 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 or verbose() then
+ log('[ ta ] key: ' .. key_tag .. ' state: '..ta.state)
+ end
+ return true
+ elseif not key_revoked then -- First time seen (NewKey)
+ rr.key_tag = key_tag
+ if force_valid then
+ rr.state = key_state.Valid
+ else
+ rr.state = key_state.AddPend
+ rr.timer = now + hold_down_time
+ end
+ if rr.state ~= key_state.Valid or verbose() then
+ log('[ ta ] key: ' .. key_tag .. ' state: '..rr.state)
+ end
+ table.insert(keyset, rr)
+ return true
+ end
+ return false
+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)
+ 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
+
+ -- 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('[ ta ] key: '..key_tag..' removed because missing for too long')
+ keep_ta = false
+
+ -- Purge pending key
+ elseif ta.state == key_state.AddPend then
+ log('[ ta ] key: '..key_tag..' purging')
+ keep_ta = false
+ end
+ log('[ ta ] key: '..key_tag..' state: '..ta.state)
+ return keep_ta
+end
+
+local active_refresh, update -- forwards
+
+-- Plan an event for refreshing the root DNSKEYs and re-scheduling itself
+local function refresh_plan(keyset, delay, is_initial)
+ local owner_str = kres.dname2str(keyset.owner) -- maybe fix converting back and forth?
+ keyset.refresh_ev = event.after(delay, function ()
+ resolve(owner_str, kres.type.DNSKEY, kres.class.IN, 'NO_CACHE',
+ function (pkt)
+ -- Schedule itself with updated timeout
+ 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)
+ end)
+ end)
+end
+
+-- Refresh the DNSKEYs from the packet, and return time to the next check.
+active_refresh = function (keyset, pkt, is_initial)
+ local retry = true
+ if 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
+ update(keyset, new_keys, is_initial)
+ retry = false
+ else
+ warn('[ ta ] active refresh failed for ' .. kres.dname2str(keyset.owner)
+ .. ' with rcode: ' .. pkt:rcode())
+ 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
+
+-- Write keyset to a file. States and timers are stored in comments.
+local function keyset_write(keyset)
+ if not keyset.filename then return false end -- not to be persisted
+ local fname_tmp = keyset.filename .. '.lock.' .. tostring(worker.pid);
+ local file = assert(io.open(fname_tmp, 'w'))
+ for i = 1, #keyset do
+ local ta = keyset[i]
+ ta.comment = ' ' .. ta.state .. ':' .. (ta.timer or '')
+ .. ' ; KeyTag:' .. ta.key_tag -- the tag is just for humans
+ local rr_str = kres.rr2str(ta) .. '\n'
+ if ta.state ~= key_state.Valid and ta.state ~= key_state.Missing then
+ rr_str = '; '..rr_str -- Invalidate key string (for older kresd versions)
+ end
+ file:write(rr_str)
+ end
+ file:close()
+ assert(os.rename(fname_tmp, keyset.filename))
+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)
+ for _, ta in pairs(tas) do
+ 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')
+ local tas, err = zonefile.file(path)
+ if not tas then
+ return tas, err
+ end
+ 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
+
+ for _, ta in pairs(tas) do
+ 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
+
+-- Replace current TAs for given owner by the "trusted" ones from passed keyset.
+-- Return the number of trusted keys for the owner.
+local function keyset_publish(keyset)
+ local store = kres.context().trust_anchors
+ local count = 0
+ C.kr_ta_del(store, keyset.owner)
+ for _, ta in ipairs(keyset) do
+ -- 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
+ if count == 0 then
+ warn('[ ta ] ERROR: no anchors are trusted for ' ..
+ kres.dname2str(keyset.owner) .. ' !')
+ end
+ return count
+end
+
+
+-- Update existing keyset; return true if successful.
+-- Param `is_initial` (bool): force .NewKey states to .Valid, i.e. init empty keyset.
+update = function (keyset, new_keys, is_initial)
+ if not new_keys then 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 trust_anchors.hold_down_time) / 1000
+ local keepset = {}
+ local keep_removed = keyset.keep_removed or trust_anchors.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, is_initial)
+ end
+ end
+
+ -- Store the keyset
+ keyset_write(keyset)
+
+ -- Start using the new TAs.
+ if keyset_publish(keyset) == 0 then
+ -- TODO: try to rebootstrap if for root?
+ return false
+ elseif verbose() then
+ log('[ ta ] refreshed trust anchors for domain ' .. kres.dname2str(keyset.owner) .. ' are:\n'
+ .. trust_anchors.summary(keyset.owner))
+ end
+
+ return true
+end
+
+local add_file = function (path, unmanaged)
+ 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
+
+ -- Bootstrap if requested and keyfile doesn't exist
+ if not unmanaged and not io.open(path, 'r') then
+ log("[ ta ] keyfile '%s': doesn't exist, bootstrapping", path);
+ 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' }
+ end
+ local keyset = trust_anchors.keysets['\0']
+ keyset.filename = path
+ if keyset.refresh_ev then event.cancel(keyset.refresh_ev) end
+ refresh_plan(keyset, 0, true)
+ return
+ end
+ if not unmanaged and path == (trust_anchors.keysets['\0'] or {}).filename then
+ return
+ end
+
+ -- Parse the file and check its sanity
+ local keyset, err = keyset_read(path)
+ if not keyset then
+ panic("[ ta ] ERROR: failed to read anchors from '%s' (%s)", path, err)
+ end
+ if not unmanaged then keyset.filename = path end
+ if not keyset[1] then
+ panic("[ ta ] ERROR: failed to read anchors from '%s'", path)
+ end
+ if not unmanaged then keyset.filename = path end
+ local owner = keyset[1].owner
+ for _, ta in ipairs(keyset) do
+ if ta.owner ~= owner then
+ panic("[ ta ] ERROR: mixed owner names found in file '%s'! " ..
+ "Do not mix %s and %s TAs in single file",
+ path, kres.dname2str(ta.owner), kres.dname2str(owner))
+ end
+ end
+ 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
+
+ -- Parse new keys, refresh eventually
+ if keyset_publish(keyset) ~= 0 and verbose() then
+ log('[ ta ] installed trust anchors for domain ' .. owner_str .. ' are:\n'
+ .. trust_anchors.summary(owner))
+ end
+ -- TODO: if failed and for root, try to rebootstrap?
+
+ refresh_plan(keyset, 10 * sec, false)
+end
+
+local function ta_str(owner)
+ local owner_str = kres.dname2str(owner) .. ' '
+ local msg = ''
+ for _, nta in pairs(trust_anchors.insecure) do
+ if owner == kres.str2dname(nta) then
+ msg = owner_str .. 'is negative trust anchor\n'
+ end
+ end
+ if not trust_anchors.keysets[owner] then
+ if #msg > 0 then -- it is normal that NTA does not have explicit TA
+ return msg
+ else
+ return owner_str .. 'has no explicit trust anchors\n'
+ end
+ end
+ if #msg > 0 then
+ msg = msg .. 'WARNING! negative trust anchor also has an explicit TA\n'
+ end
+ for _, ta in ipairs(trust_anchors.keysets[owner]) do
+ msg = msg .. kres.rr2str(ta) .. '\n'
+ end
+ return msg
+end
+
+-- TA store management, for user docs see ../README.rst
+trust_anchors = {
+ -- [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 = {},
+
+ -- 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',
+ -- change empty string to nil
+ keyfile_default = ('@KEYFILE_DEFAULT@' ~= '' and '@KEYFILE_DEFAULT@') or nil,
+
+ -- 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,
+
+ -- Add DS/DNSKEY record(s) (unmanaged)
+ add = function (keystr)
+ local ret = trustanchor(keystr)
+ if verbose() then log(trust_anchors.summary()) end
+ return ret
+ end,
+ -- Negative TA management
+ set_insecure = function (list)
+ assert(type(list) == 'table', 'parameter must be list of domain names (e.g. {"a.test", "b.example"})')
+ 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,
+ summary = function (single_owner)
+ if single_owner then -- single domain
+ return ta_str(single_owner)
+ end
+
+ -- all domains
+ local msg = ''
+ local ta_count = 0
+ local seen = {}
+ for _, nta_str in pairs(trust_anchors.insecure) do
+ local owner = kres.str2dname(nta_str)
+ seen[owner] = true
+ msg = msg .. ta_str(owner)
+ end
+ for owner, _ in pairs(trust_anchors.keysets) do
+ if not seen[owner] then
+ ta_count = ta_count + 1
+ msg = msg .. ta_str(owner)
+ end
+ end
+ if ta_count == 0 then
+ msg = msg .. 'No valid trust anchors, DNSSEC validation is disabled\n'
+ end
+ return msg
+ end,
+}
+
+-- 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,
+})
+
+return trust_anchors
diff --git a/daemon/lua/trust_anchors.test/bootstrap.test.lua b/daemon/lua/trust_anchors.test/bootstrap.test.lua
new file mode 100644
index 0000000..d5d0218
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/bootstrap.test.lua
@@ -0,0 +1,108 @@
+-- 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'),
+}
diff --git a/daemon/lua/trust_anchors.test/err_attr_extra_attr.xml b/daemon/lua/trust_anchors.test/err_attr_extra_attr.xml
new file mode 100644
index 0000000..2a87957
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_attr_extra_attr.xml
@@ -0,0 +1,16 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/err_attr_validfrom_invalid.xml b/daemon/lua/trust_anchors.test/err_attr_validfrom_invalid.xml
new file mode 100644
index 0000000..5a4c68c
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_attr_validfrom_invalid.xml
@@ -0,0 +1,16 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/err_attr_validfrom_missing.xml b/daemon/lua/trust_anchors.test/err_attr_validfrom_missing.xml
new file mode 100644
index 0000000..1261b09
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_attr_validfrom_missing.xml
@@ -0,0 +1,16 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/err_elem_extra.xml b/daemon/lua/trust_anchors.test/err_elem_extra.xml
new file mode 100644
index 0000000..150a3b1
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_elem_extra.xml
@@ -0,0 +1,17 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/err_elem_missing.xml b/daemon/lua/trust_anchors.test/err_elem_missing.xml
new file mode 100644
index 0000000..899e1d0
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_elem_missing.xml
@@ -0,0 +1,16 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/err_multi_ta.xml b/daemon/lua/trust_anchors.test/err_multi_ta.xml
new file mode 100644
index 0000000..20cd73f
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/err_multi_ta.xml
@@ -0,0 +1,19 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/ok0_badtimes.xml b/daemon/lua/trust_anchors.test/ok0_badtimes.xml
new file mode 100644
index 0000000..4535a41
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok0_badtimes.xml
@@ -0,0 +1,16 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/ok1.xml b/daemon/lua/trust_anchors.test/ok1.xml
new file mode 100644
index 0000000..117495c
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok1.xml
@@ -0,0 +1,10 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/ok1_expired1.xml b/daemon/lua/trust_anchors.test/ok1_expired1.xml
new file mode 100644
index 0000000..f1269da
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok1_expired1.xml
@@ -0,0 +1,16 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/ok1_notyet1.xml b/daemon/lua/trust_anchors.test/ok1_notyet1.xml
new file mode 100644
index 0000000..7b5881b
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok1_notyet1.xml
@@ -0,0 +1,16 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/ok2.xml b/daemon/lua/trust_anchors.test/ok2.xml
new file mode 100644
index 0000000..149f6b5
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/ok2.xml
@@ -0,0 +1,16 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/regen.sh b/daemon/lua/trust_anchors.test/regen.sh
new file mode 100755
index 0000000..09b334c
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/regen.sh
@@ -0,0 +1,2 @@
+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
diff --git a/daemon/lua/trust_anchors.test/unsupp_nonroot.xml b/daemon/lua/trust_anchors.test/unsupp_nonroot.xml
new file mode 100644
index 0000000..51b3c0a
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/unsupp_nonroot.xml
@@ -0,0 +1,10 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/unsupp_xml_v11.xml b/daemon/lua/trust_anchors.test/unsupp_xml_v11.xml
new file mode 100644
index 0000000..87a4b57
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/unsupp_xml_v11.xml
@@ -0,0 +1,10 @@
+<?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>
diff --git a/daemon/lua/trust_anchors.test/webserv.lua b/daemon/lua/trust_anchors.test/webserv.lua
new file mode 100644
index 0000000..458d3e5
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/webserv.lua
@@ -0,0 +1,234 @@
+-- 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, 'cert missing')
+ assert(conf.key, 'private key missing')
+ -- Check if a cert file was specified
+ -- Read x509 certificate
+ local f = io.open(conf.cert, 'r')
+ if f then
+ crt = assert(x509.new(f:read('*all')))
+ f:close()
+ -- Continue reading key file
+ if crt then
+ f = io.open(conf.key, 'r')
+ key = assert(pkey.new(f:read('*all')))
+ f:close()
+ end
+ end
+ -- Check loaded certificate
+ assert(crt and key,
+ string.format('failed to load certificate "%s"', conf.cert))
+ end
+ -- Compose server handler
+ local routes = route(conf.endpoints or M.endpoints)
+ -- Check if UNIX socket path is used
+ local addr_str
+ if not conf.path then
+ conf.host = conf.host or 'localhost'
+ conf.port = conf.port or 8053
+ addr_str = string.format('%s@%d', conf.host, conf.port)
+ else
+ if conf.host or conf.port then
+ error('either "path", or "host" and "port" must be provided')
+ end
+ addr_str = conf.path
+ end
+ -- Create TLS context and start listening
+ local s, err = http_server.listen {
+ -- cq = worker.bg_worker.cq,
+ host = conf.host,
+ port = conf.port,
+ path = conf.path,
+ v6only = conf.v6only,
+ unlink = conf.unlink,
+ reuseaddr = conf.reuseaddr,
+ reuseport = conf.reuseport,
+ client_timeout = conf.client_timeout or 5,
+ ctx = crt and tlscontext(crt, key),
+ tls = conf.tls,
+ onstream = routes,
+ -- Log errors, but do not throw
+ onerror = function(myserver, context, op, err, errno) -- luacheck: ignore 212
+ local msg = '[http] ' .. op .. ' on ' .. tostring(context) .. ' failed'
+ if err then
+ msg = msg .. ': ' .. tostring(err)
+ end
+ print(msg)
+ end,
+ }
+ -- Manually call :listen() so that we are bound before calling :localname()
+ if s then
+ err = select(2, s:listen())
+ end
+ assert(not err, string.format('failed to listen on %s: %s', addr_str, err))
+ return s
+end
+
+-- init
+local files = {
+ 'ok0_badtimes.xml',
+ 'ok1.xml',
+ 'ok1_expired1.xml',
+ 'ok1_notyet1.xml',
+ 'ok2.xml',
+ 'err_attr_validfrom_missing.xml',
+ 'err_attr_validfrom_invalid.xml',
+ 'err_attr_extra_attr.xml',
+ 'err_elem_missing.xml',
+ 'err_elem_extra.xml',
+ 'err_multi_ta.xml',
+ 'unsupp_nonroot.xml',
+ 'unsupp_xml_v11.xml'
+}
+
+-- Export static pages specified at command line
+for _, name in ipairs(files) do
+ local fd = io.open(name)
+ assert(fd, string.format('unable to open file "%s"', name))
+ M.endpoints['/' .. name] = { 'text/xml', fd:read('*a') }
+ fd:close()
+end
+
+local server = M.add_interface({
+ host = 'localhost',
+ port = 8080,
+ tls = true,
+ cert = 'x509/server.pem',
+ key = 'x509/server-key.pem'
+ })
+
+server:loop()
diff --git a/daemon/lua/trust_anchors.test/x509/ca-key.pem b/daemon/lua/trust_anchors.test/x509/ca-key.pem
new file mode 100644
index 0000000..28b1f52
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/ca-key.pem
@@ -0,0 +1,182 @@
+Public Key Info:
+ Public Key Algorithm: RSA
+ Key Security Level: High (3072 bits)
+
+modulus:
+ 00:cd:d7:b2:a4:37:ae:47:8b:c2:e2:24:7a:2e:77:4a
+ 9a:aa:8e:5a:f5:68:80:f0:7c:68:08:50:30:3a:f9:77
+ bb:99:a6:67:f7:ce:45:6a:c9:11:43:81:4e:a7:92:53
+ 58:53:0c:76:fe:81:9c:a5:c8:32:e9:2e:5d:1d:16:3e
+ 5d:b2:21:c6:65:d7:33:c1:04:81:7a:8e:8c:c7:2e:8c
+ d0:73:bf:37:43:b7:1b:07:90:cb:a3:6e:7c:67:76:be
+ b0:20:28:5b:5d:d7:b3:b2:09:5e:2b:23:76:fd:32:26
+ c7:cf:de:e8:0c:5b:c2:bc:a8:52:e2:58:56:89:ad:57
+ 63:b0:01:86:b3:d1:3d:e0:6c:13:ff:b5:e6:37:f4:87
+ 64:f5:d8:9e:8c:7d:48:43:60:5c:bb:bb:73:cc:d3:3f
+ ad:76:44:ff:08:25:b8:dc:c4:47:c2:29:29:b8:00:4b
+ a7:01:84:93:40:9c:80:35:78:7d:b2:bb:70:58:b8:7a
+ d9:60:7c:e6:d6:66:36:cb:62:4d:84:e8:b9:9e:62:2f
+ 97:2c:cd:c2:28:6e:32:a5:c9:33:a1:8d:96:41:05:5c
+ 84:7a:82:80:4e:df:8b:98:75:ba:84:84:a3:c7:9c:41
+ c8:19:d8:af:37:28:f2:e8:c2:dc:b9:b2:fa:7c:26:b6
+ 31:bd:2f:e7:c3:aa:5d:c4:03:fb:db:42:3d:fe:2e:15
+ 9e:23:40:b2:e5:e1:7e:a0:e1:5d:d7:ff:aa:ed:e9:09
+ 99:55:2d:48:4c:67:95:b6:3f:d8:87:c7:b3:b6:ae:b0
+ 45:42:9c:6a:3f:a7:8e:d2:79:e7:2e:51:d6:f1:25:e8
+ 4f:4a:e4:cc:4e:2e:ae:d7:a6:b2:8a:d5:5d:76:42:ff
+ 19:eb:0d:b9:a0:8c:76:ed:9d:e6:e9:83:0c:35:d7:d3
+ 4c:16:2a:6b:25:1f:7a:f1:6d:6f:33:cf:21:8c:9f:a8
+ 43:0b:11:d3:da:cf:ec:1e:64:54:c1:3e:12:13:f1:eb
+ b5:
+
+public exponent:
+ 01:00:01:
+
+private exponent:
+ 00:ab:ca:e1:64:fc:b3:8f:32:ad:ab:5f:16:39:c1:85
+ 9a:1f:ce:3f:4b:a1:b4:3b:01:19:32:16:fa:a9:bd:9a
+ 98:0f:5c:3a:59:2e:e5:f0:81:6e:cf:10:14:3c:f6:7a
+ 68:b4:a7:2a:88:ae:53:b6:68:a7:54:c5:45:21:09:77
+ 73:6b:3f:94:fd:59:e5:ef:a9:7b:06:76:02:38:1a:39
+ 9b:9f:7e:6e:f9:2c:d0:7a:37:f6:3c:a7:f1:5b:c8:56
+ cd:57:89:56:f7:b3:16:5d:f8:43:87:6d:49:d9:77:09
+ b6:a9:5e:37:fc:58:78:e6:4a:f5:21:c2:e8:36:6f:5d
+ 07:ed:d4:d8:3f:2a:da:a1:7a:92:16:50:11:9b:91:91
+ 8e:49:40:48:d2:a7:9c:af:de:b4:86:59:a4:03:c6:2a
+ d6:f2:66:13:13:11:a8:7f:57:b1:2b:f2:7e:c6:fa:38
+ 29:61:b2:eb:d2:78:ca:e7:d5:6f:d2:c3:6c:b8:1e:ff
+ 26:fc:47:62:46:b6:03:c1:d9:a0:03:4f:16:5f:fe:65
+ e7:56:a2:21:38:b0:34:17:45:00:56:26:e8:5e:8b:fe
+ e0:fc:fc:a0:3e:2c:00:fb:c3:f8:dc:07:05:73:59:59
+ fd:b6:7a:c3:cd:4d:da:5a:1c:38:17:2e:15:8f:50:18
+ 52:bb:d2:a9:fe:88:6f:d2:71:d3:7e:74:85:a3:88:ba
+ 99:79:4c:ce:36:23:63:0a:17:16:00:da:fb:76:fb:90
+ bb:c1:08:77:43:0d:ce:81:a8:89:47:ea:bc:f0:40:65
+ 07:0d:d0:9c:5e:02:b5:2b:89:cb:65:56:c3:24:b0:e1
+ 39:30:20:aa:03:4c:20:7b:36:14:24:cb:8b:64:74:fc
+ 00:2e:de:d6:d1:9b:93:17:d1:98:37:7f:6a:cd:42:d3
+ 20:b2:92:2e:96:1e:3c:04:3e:99:1c:c7:03:d4:b9:87
+ 98:e2:a5:41:b8:3f:0c:1a:94:54:16:4d:2b:83:58:de
+ 01:
+
+prime1:
+ 00:f9:82:8d:c3:92:3d:bd:52:6b:ff:4e:0b:97:b3:8f
+ a2:56:53:49:05:1e:5b:20:cc:1b:fc:a4:0e:eb:ad:ce
+ 1f:79:7a:3c:e6:b3:6d:1e:1a:bf:5a:01:76:c9:14:6c
+ 0b:2d:6e:b2:56:55:b4:4d:f8:da:15:68:eb:07:59:43
+ c7:de:8b:01:14:8f:6d:3b:c5:d6:7d:86:f1:ba:9a:88
+ 0c:bc:06:96:2a:94:59:d4:e1:eb:15:dd:20:9e:98:78
+ 58:ca:ef:19:a7:52:c4:a0:35:91:d8:0b:3b:d2:93:aa
+ 60:bc:44:c3:a5:ac:1c:f4:38:3e:60:79:da:c3:53:9a
+ 6b:87:87:77:36:09:a1:27:b2:60:3e:aa:f3:73:d4:a7
+ af:f7:74:9e:c7:19:b1:e4:75:ed:79:56:67:53:05:1c
+ 4e:55:f5:be:6a:9c:3a:4b:15:73:55:90:7c:a2:e9:45
+ 07:d5:eb:a0:3f:da:d6:7f:ae:4b:62:6d:b5:8d:af:48
+ 31:
+
+prime2:
+ 00:d3:32:5e:b2:ef:2e:bf:d1:57:85:28:bb:5c:37:c0
+ 8a:ed:46:47:23:4e:22:72:2f:de:0a:a6:36:64:8f:9e
+ f5:67:a3:5d:d4:2d:2b:e2:3c:c7:b8:0c:79:37:bf:e6
+ 67:96:84:4c:be:98:1f:86:e0:3d:c9:4a:b6:50:de:2f
+ 90:61:25:74:49:ff:33:a7:93:3a:12:2b:c2:2c:38:29
+ 9f:16:d8:20:16:77:e6:04:27:23:44:35:5a:a2:a1:72
+ 40:91:b6:39:5f:e6:57:9f:59:6a:e4:5c:da:50:c7:4f
+ fe:df:1f:40:3c:e7:05:f6:f9:52:e9:c4:2c:e0:68:46
+ 47:6e:52:76:bc:c1:19:7c:2d:50:2f:ef:53:e1:67:7c
+ b8:7e:84:72:6d:60:2c:ff:ff:7f:7a:ec:1e:54:8c:b1
+ a8:64:0b:54:f5:c3:4e:dd:dc:22:3b:54:05:7f:cc:32
+ 64:d5:87:15:9f:f8:9f:07:83:4e:a0:d2:13:48:6b:be
+ c5:
+
+coefficient:
+ 2e:c4:04:3b:cf:3f:4a:dd:f0:32:e4:fc:93:fe:ef:b8
+ 83:14:8e:a9:6b:ba:73:28:b6:b1:49:57:67:d7:ca:39
+ f0:da:23:99:df:3d:9b:8c:7b:c9:9a:fe:22:69:29:87
+ a3:ad:2a:6b:a7:5a:42:09:57:fd:8e:55:0a:1a:e9:36
+ 02:e7:b3:47:82:41:c1:21:b2:8b:6a:35:30:60:17:c3
+ a5:3b:cc:2a:cb:e1:7a:23:14:6c:8a:87:29:49:8a:0a
+ c3:34:a8:aa:64:92:74:82:f1:01:3a:00:2e:bf:d6:d3
+ 00:86:01:ee:84:83:42:8d:c7:b0:88:ac:62:12:9d:b5
+ 18:28:76:13:34:24:92:fb:a9:57:e5:4c:8a:a9:bb:73
+ a5:02:64:9b:73:4c:18:6b:29:e4:ba:04:da:66:ef:b4
+ 0c:46:fd:55:ff:62:5b:76:80:a9:13:29:c1:4b:43:51
+ 0b:44:d3:01:9d:c5:e5:6b:ac:a0:e5:b9:0c:41:08:49
+
+
+exp1:
+ 00:9d:50:d4:63:4f:cc:7f:96:fd:22:de:a9:6e:7f:b7
+ 7b:4a:64:7e:b0:ac:80:16:80:ba:d0:a2:fc:09:5a:ef
+ 90:66:be:4c:b1:c4:c5:72:ea:b8:65:5e:70:ef:bd:61
+ 95:f6:92:49:fd:27:52:64:ab:17:8d:d2:36:05:cf:21
+ 6e:5e:81:54:30:0b:72:7a:f2:75:17:76:42:e9:3d:cf
+ b3:ff:c5:43:5f:1b:64:3c:56:29:2c:02:dd:33:41:bc
+ f7:77:14:24:1f:9c:8e:fe:d8:67:d9:48:d3:f2:24:4a
+ 93:6d:81:09:be:66:73:67:04:23:48:1e:ec:70:a5:40
+ c2:b5:94:12:f4:ce:43:7e:cc:f7:e3:eb:53:4e:5f:f1
+ 4e:80:7e:56:32:00:a0:6a:04:74:b3:41:68:2a:2f:19
+ b6:c6:7a:08:12:1d:e8:9f:38:aa:1c:73:da:31:b9:54
+ 5d:e9:62:04:0a:de:c6:c9:80:32:65:9f:f8:8d:99:cb
+ b1:
+
+exp2:
+ 77:f1:8f:5e:c8:00:20:e8:5f:70:3c:a5:cb:c5:ce:10
+ 18:99:65:25:63:e6:a4:3b:13:3b:b1:12:0b:22:96:00
+ 81:8f:82:66:52:11:2e:37:9e:a5:a4:4f:e3:9d:94:d9
+ 17:de:a3:47:4b:55:fc:5f:b6:37:6f:bb:03:4a:6c:70
+ ee:fb:3a:84:1b:ef:d6:28:03:3e:f6:a3:1c:41:f9:41
+ 64:d3:f1:c5:50:ea:cd:48:fc:fa:6b:e2:c1:a2:37:24
+ fc:9c:25:11:95:dc:05:9f:de:d9:3a:f2:b2:15:c0:14
+ db:da:ff:bc:96:e8:08:4f:a9:0b:22:62:ea:3f:ce:4a
+ 1b:19:b2:5d:98:bd:44:8c:e7:91:91:b6:6d:b9:3c:57
+ fb:62:75:2a:31:08:dd:8a:d9:77:92:49:d7:72:e0:22
+ e6:4e:99:3d:ca:62:2c:16:2d:4a:cc:79:23:0f:71:3f
+ 5b:15:90:ee:7d:60:b3:ff:9a:d4:32:fa:c8:92:88:91
+
+
+
+Public Key PIN:
+ pin-sha256:D8K7fp8E6Fsg1NjgFkNjJA/Mow1IQUvondTb816FnDc=
+Public Key ID:
+ sha256:0fc2bb7e9f04e85b20d4d8e0164363240fcca30d48414be89dd4dbf35e859c37
+ sha1:3017e1bccfde068e418672642d1e181a90988264
+
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/daemon/lua/trust_anchors.test/x509/ca.pem b/daemon/lua/trust_anchors.test/x509/ca.pem
new file mode 100644
index 0000000..70e0fd5
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/ca.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEFzCCAn+gAwIBAgIUBSlZzv0lYdpFv+zOqzsn3zZn7q0wDQYJKoZIhvcNAQEL
+BQAwIzEhMB8GA1UEAxMYS25vdCBSZXNvbHZlciB0ZXN0aW5nIENBMB4XDTE5MDEw
+MzE2MjczM1oXDTIwMDEwMzE2MjczM1owIzEhMB8GA1UEAxMYS25vdCBSZXNvbHZl
+ciB0ZXN0aW5nIENBMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAzdey
+pDeuR4vC4iR6LndKmqqOWvVogPB8aAhQMDr5d7uZpmf3zkVqyRFDgU6nklNYUwx2
+/oGcpcgy6S5dHRY+XbIhxmXXM8EEgXqOjMcujNBzvzdDtxsHkMujbnxndr6wIChb
+XdezsgleKyN2/TImx8/e6AxbwryoUuJYVomtV2OwAYaz0T3gbBP/teY39Idk9die
+jH1IQ2Bcu7tzzNM/rXZE/wgluNzER8IpKbgAS6cBhJNAnIA1eH2yu3BYuHrZYHzm
+1mY2y2JNhOi5nmIvlyzNwihuMqXJM6GNlkEFXIR6goBO34uYdbqEhKPHnEHIGdiv
+Nyjy6MLcubL6fCa2Mb0v58OqXcQD+9tCPf4uFZ4jQLLl4X6g4V3X/6rt6QmZVS1I
+TGeVtj/Yh8eztq6wRUKcaj+njtJ55y5R1vEl6E9K5MxOLq7XprKK1V12Qv8Z6w25
+oIx27Z3m6YMMNdfTTBYqayUfevFtbzPPIYyfqEMLEdPaz+weZFTBPhIT8eu1AgMB
+AAGjQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcEADAdBgNVHQ4E
+FgQUMBfhvM/eBo5BhnJkLR4YGpCYgmQwDQYJKoZIhvcNAQELBQADggGBAMpUHwLa
+v9sDAkhZ+Wp/I5LdzP/7i4f/EI8tEh+GYeoWNvW9FkvaW1ydU6ZPx+j2hzZRCpIR
+WTT/w0dB7d+2EOUJKrytPpx5O/uYIYOqCYbWAYIF6Vdk0vakwmWSg9YsdwLfDHfI
+/K6vt3cIPLCzSXQdimHzKd4+6pXZcjsEFSpmueaHk08HGErEVeWgG3Ro3XwuBJNt
+9QUN7fF8DAi3705/NuoF1fR9MvuorR3KBgYKFO/7F6gxgTVHXfwK8iRN0YZLPU02
+eUUs7zq8FS87chuq9ABodBmsmkVI6671N57U8duxVacSuSHEyYdHamYznBdGnCMv
+GCvn0VyUuqXe45LoRK4um4MC/3tPp70bY1LqyWXoCLMOdnObzI341PXoFvI/ScYa
+La2lkRz4wMqBecM0fLOJqshdDt6ZgEP3YMHgIhM87VR4savECnfH+RHAIZt/oEEt
+dJmAC9H2f2/zoxIXcRrGbU2kB5wBYW69qRzlOOtScimCmHdtVK/iAzg4KQ==
+-----END CERTIFICATE-----
diff --git a/daemon/lua/trust_anchors.test/x509/ca.tmpl b/daemon/lua/trust_anchors.test/x509/ca.tmpl
new file mode 100644
index 0000000..6de507d
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/ca.tmpl
@@ -0,0 +1,3 @@
+cn = Knot Resolver testing CA
+ca
+cert_signing_key
diff --git a/daemon/lua/trust_anchors.test/x509/gen.sh b/daemon/lua/trust_anchors.test/x509/gen.sh
new file mode 100755
index 0000000..10ecad3
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/gen.sh
@@ -0,0 +1,11 @@
+# CA
+certtool --generate-privkey > ca-key.pem
+certtool --generate-self-signed --load-privkey ca-key.pem --template ca.tmpl --outfile ca.pem
+
+# server cert signed by CA above
+certtool --generate-privkey > server-key.pem
+certtool --generate-certificate --load-privkey server-key.pem --load-ca-certificate ca.pem --load-ca-privkey ca-key.pem --template server.tmpl --outfile server.pem
+
+# wrong CA - unrelated to others
+certtool --generate-privkey > wrongca-key.pem
+certtool --generate-self-signed --load-privkey wrongca-key.pem --template wrongca.tmpl --outfile wrongca.pem
diff --git a/daemon/lua/trust_anchors.test/x509/server-key.pem b/daemon/lua/trust_anchors.test/x509/server-key.pem
new file mode 100644
index 0000000..4410e27
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/server-key.pem
@@ -0,0 +1,182 @@
+Public Key Info:
+ Public Key Algorithm: RSA
+ Key Security Level: High (3072 bits)
+
+modulus:
+ 00:ba:f8:5c:15:1b:85:bd:32:5e:59:c4:ae:73:a2:b0
+ 67:06:07:69:e6:88:04:4d:98:45:5d:b6:43:78:8b:1f
+ 53:b3:22:0a:d1:b2:48:78:96:38:b8:98:1a:08:41:12
+ 2a:38:cc:3a:ae:b8:9d:73:ea:7c:ba:7a:08:ea:bc:24
+ f3:9b:1f:81:eb:54:6e:d9:16:b2:15:bc:66:36:78:43
+ 0c:06:8e:ba:aa:b6:f4:fb:12:d4:1f:04:22:b3:5b:03
+ 8f:b7:a6:25:df:58:c4:ca:b3:09:c4:54:b2:a4:25:fa
+ af:a6:24:d8:83:9f:18:fc:08:6c:da:f4:ae:68:57:75
+ d6:a8:c1:bf:db:39:f7:b0:c2:63:8d:95:18:01:0a:ee
+ 81:d0:e2:76:d5:52:5c:25:f8:aa:0c:a8:7f:34:ca:e2
+ bc:0e:34:be:63:7f:4a:01:90:36:5c:4d:d9:d9:ee:05
+ fc:e2:c8:a1:32:76:dc:39:54:33:dd:c4:b8:1e:a1:f5
+ 23:c1:7f:3b:6a:ff:84:98:60:1f:53:86:bf:ac:d6:4e
+ 58:9a:91:aa:13:1e:a5:9e:61:eb:ba:8c:35:da:89:4d
+ 48:7b:82:cf:95:d4:68:fd:24:aa:43:9c:3a:5b:86:f6
+ 7d:2d:41:dc:63:36:2c:c2:c5:0b:49:04:46:f3:c4:b9
+ e0:19:e4:06:80:4c:9f:9f:1a:6b:9a:88:42:6c:6c:a9
+ 48:1c:9d:ff:a1:71:2c:44:5a:e5:3f:b4:bc:b6:43:db
+ 47:1f:65:15:41:3f:d0:7a:8f:c1:1c:f0:93:11:c7:13
+ 74:5f:7c:47:e9:2c:bc:d0:7a:fc:c0:89:6e:e9:1e:82
+ 85:ef:a7:af:57:d3:fc:af:c7:9a:34:06:60:2e:db:bc
+ 01:d5:08:63:8e:07:27:51:3a:17:0a:71:22:56:eb:e9
+ af:f6:48:31:42:87:2d:95:05:ad:f8:c6:76:bc:17:2f
+ 50:47:68:95:4a:6b:4d:ce:ba:51:2b:1e:e1:b3:d5:79
+ 27:
+
+public exponent:
+ 01:00:01:
+
+private exponent:
+ 3a:95:8d:8c:d3:95:e1:45:82:08:d0:b6:f4:e6:ba:2d
+ 5d:d7:3e:d8:8c:30:04:fc:3c:67:f3:af:4a:7b:15:32
+ cd:c3:51:ee:88:d5:6a:6b:6f:94:6f:9c:60:8f:bb:18
+ 5d:b9:a8:7d:8c:bd:51:4f:dd:0b:35:27:cc:1d:6d:da
+ a2:f5:89:68:ea:88:dd:1e:de:68:2f:23:3b:d8:0c:f2
+ 1c:af:e6:4f:8d:8a:4f:8d:83:c7:c0:2c:fb:53:fe:56
+ ae:ed:b1:9a:3d:1f:54:80:2b:04:48:a1:bd:0b:65:74
+ 5a:33:db:5f:ca:9f:32:81:95:52:3a:2e:d7:e4:e7:b8
+ 7e:22:4f:72:6a:c4:70:af:48:30:59:d2:2e:a9:75:38
+ 59:f2:4b:d7:e5:b9:97:f0:45:a0:37:bf:bc:14:02:5d
+ 78:7a:10:59:ee:cd:8a:95:dc:62:a9:c6:24:22:0f:e0
+ 40:53:2b:27:3b:d5:1f:65:81:e6:f0:37:dc:e9:54:7c
+ f8:81:f0:e0:f7:80:05:ac:91:14:5b:5d:03:14:c2:b9
+ 74:64:f6:ab:6e:a5:ce:e7:bc:56:58:41:30:31:8e:e9
+ f5:6e:31:62:31:69:a8:17:44:89:b3:b9:95:5c:cd:fc
+ 40:86:2c:a1:1d:46:48:94:26:ae:35:bf:14:0e:ae:49
+ 2d:56:5f:bd:e0:d6:f5:03:ed:df:a5:a9:43:89:5c:01
+ 33:5e:e6:3e:80:da:9f:df:2d:c7:e8:13:60:32:27:0e
+ 77:11:fd:43:94:2b:6e:6b:12:79:aa:59:e6:54:b6:47
+ dc:78:7e:fe:4b:40:32:12:3a:cc:6d:d2:9f:49:fc:b2
+ dc:6f:d0:01:03:1d:1d:a8:e5:ee:60:d7:8c:26:7f:73
+ 8a:c0:81:95:ff:b6:c5:5e:d7:6b:50:1f:02:61:3b:31
+ f4:41:da:0a:a7:c6:ea:d2:a2:c4:b5:6d:ce:3f:e0:3b
+ 03:a7:38:37:db:88:82:18:09:a2:da:cf:77:3a:33:f9
+
+
+prime1:
+ 00:f7:94:41:16:98:0c:cb:a1:31:a8:33:38:96:03:f1
+ 33:bf:b7:03:1b:28:bc:43:62:40:77:e6:d9:75:64:77
+ ff:64:51:34:9c:65:3a:ca:10:0f:3e:ce:37:ef:14:85
+ d5:7d:f5:ee:29:f3:76:02:c4:dd:52:91:5c:2e:7e:ad
+ 76:6b:65:e9:60:fc:7c:30:64:9a:bb:65:22:39:a3:a0
+ 54:78:9b:15:2d:a7:e1:36:25:2f:29:ee:01:d7:1d:b6
+ f6:07:2c:b6:8f:b4:66:82:48:c0:3a:d8:2f:bb:d7:bb
+ f4:f0:54:59:ea:66:d9:e9:f8:f2:dc:a8:76:d3:5f:30
+ 8e:18:37:2e:b8:a1:64:b6:d6:14:8e:9d:ef:d2:90:45
+ a8:fe:f9:a6:75:42:9b:69:d7:99:df:2e:f9:58:b8:a0
+ 7c:57:d6:10:c4:7b:b3:30:d0:4e:80:74:37:1a:86:bb
+ d8:bf:90:9f:62:be:f1:22:bf:0c:cf:d7:c0:cf:4c:e0
+ 4b:
+
+prime2:
+ 00:c1:54:5d:70:8f:b2:93:c7:68:a0:a3:0f:3b:00:9d
+ f7:99:96:2f:6f:46:11:7a:71:e8:be:84:b4:57:3f:32
+ d0:bc:09:64:04:61:e7:ce:a9:e2:60:a4:6e:18:8a:e1
+ f0:05:88:e0:a1:ae:6e:9e:db:f9:39:8d:04:b4:12:a5
+ 0d:fe:bb:95:ce:bc:13:5a:3a:a9:18:a3:7b:70:39:99
+ d4:a4:eb:92:22:6a:85:66:f9:50:b7:fe:10:b9:a7:d3
+ f3:2b:96:66:93:e8:00:ae:c7:eb:cb:08:cc:3e:d3:ab
+ c3:aa:a4:4a:36:72:07:c8:eb:6b:75:17:f2:1f:e3:32
+ f0:db:ce:8b:5b:93:e7:dc:58:56:ab:38:5e:72:3c:3b
+ b1:08:c5:51:b0:fe:21:d9:a0:63:49:5b:bc:c6:fe:de
+ 99:1d:9e:84:37:dc:4f:63:0d:ff:0c:b5:33:8e:18:74
+ 6d:9f:07:45:ca:1b:15:c7:83:64:6f:2b:39:73:87:59
+ 15:
+
+coefficient:
+ 25:67:50:eb:3e:0e:3c:1d:a4:71:da:11:9b:64:59:83
+ ad:df:a3:82:07:aa:3e:a2:c6:cc:c1:6f:cf:5e:09:f7
+ 18:f8:a1:75:6a:43:99:c1:a8:01:2c:43:b8:d4:7f:5a
+ fe:a5:aa:3d:18:a8:39:5d:87:f6:88:fb:22:a4:13:09
+ 92:bb:8f:e4:23:5f:07:e7:3c:11:2a:55:38:35:86:ad
+ 63:44:ae:7b:25:1a:27:58:47:b2:ca:a2:07:04:d5:e5
+ c9:af:b1:09:da:1d:5c:49:0b:07:cd:93:b2:70:1e:9a
+ 2d:90:e0:80:75:93:8f:4e:97:1a:c6:af:a4:6c:9d:fe
+ 5c:41:80:5c:3f:2e:c7:b3:7f:ed:36:78:46:50:ef:c7
+ 6f:fe:1b:b0:60:f0:3a:d9:67:7f:23:74:9d:c4:10:70
+ 00:cd:27:a3:72:45:35:17:a5:86:17:be:e6:4a:70:a4
+ 03:9d:ea:70:50:64:65:f1:30:56:ce:0e:b7:e2:58:a7
+
+
+exp1:
+ 00:9e:dc:1e:37:a5:30:f0:a8:69:f8:87:85:53:9d:0b
+ f4:2c:9b:fd:fe:3b:51:31:db:a5:8a:4a:32:56:c5:34
+ ca:47:50:63:f5:c6:6e:c6:a1:2f:67:19:63:82:a1:24
+ 8f:2c:d7:d5:0e:4e:0d:f7:10:e3:02:cc:0a:de:3a:a2
+ 8b:4d:b6:82:dd:9c:a5:03:58:4a:80:dc:0f:ed:f4:34
+ 38:7f:7a:e3:47:fc:64:e2:1d:51:fa:11:a2:54:a9:d8
+ 70:5d:82:2f:52:5e:6b:38:45:fe:32:c3:ed:3d:16:dc
+ 9f:fa:65:e5:9c:26:8a:c5:3a:dc:7b:02:0d:dc:eb:43
+ 78:a9:c9:1e:cd:91:a1:d2:3f:e3:c8:ef:46:a7:51:b3
+ a1:10:9a:98:58:bd:78:83:9d:b8:3a:21:26:15:eb:c1
+ ee:87:5d:f0:3c:63:33:43:ab:25:f3:fe:9e:2d:03:2f
+ 1d:91:2d:f7:57:a1:35:91:1a:0d:da:7f:92:54:71:fb
+ a9:
+
+exp2:
+ 2b:a1:e5:c0:cc:bd:a9:fa:9c:53:7c:d9:a8:20:58:86
+ 94:24:40:2a:65:ee:f5:ea:95:73:c2:31:8d:6b:57:05
+ a3:1a:9f:77:19:bd:9e:77:da:fe:a2:bd:b2:4e:4d:f5
+ c4:da:02:90:9a:f4:9e:67:d9:14:b3:0d:f7:b2:29:8c
+ 42:0c:86:1f:f5:74:8c:ad:a6:92:47:fb:48:f5:c7:11
+ 25:f3:80:b4:c1:c3:bf:dc:ce:e9:e7:ae:50:a8:5e:fe
+ 87:bc:d7:03:d4:9d:aa:d4:b6:13:c9:b5:87:0c:70:bc
+ a5:5b:94:e0:3a:d6:24:f3:74:fa:25:60:60:ef:ff:04
+ 3b:27:9f:6e:18:b0:80:9b:73:5c:0b:49:cd:90:68:8c
+ 69:05:57:8d:91:9d:84:27:5d:a1:25:d2:32:3b:3d:73
+ e3:2a:6e:7e:c8:fb:25:c8:f7:e2:1f:57:36:5f:b0:8f
+ 39:10:04:21:3c:01:ab:58:ad:27:25:e3:3e:7e:b2:8d
+
+
+
+Public Key PIN:
+ pin-sha256:39HEiK68VoVnfvhMoLHp8J9yLV3u4CjiDlcNu1pmUxw=
+Public Key ID:
+ sha256:dfd1c488aebc5685677ef84ca0b1e9f09f722d5deee028e20e570dbb5a66531c
+ sha1:fab196e0eaaf8f48c8f4fed07c97e4aabba3a1ed
+
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/daemon/lua/trust_anchors.test/x509/server.pem b/daemon/lua/trust_anchors.test/x509/server.pem
new file mode 100644
index 0000000..47fb6a4
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/server.pem
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEezCCAuOgAwIBAgIUAYjlKE92sFzGxwB2+PN+dV2rKOEwDQYJKoZIhvcNAQEL
+BQAwIzEhMB8GA1UEAxMYS25vdCBSZXNvbHZlciB0ZXN0aW5nIENBMB4XDTE5MDEw
+MzE2MjczM1oXDTIwMDEwMzE2MjczM1owPDESMBAGA1UEAxMJbG9jYWxob3N0MSYw
+JAYDVQQKEx1GYWtlIEROUyByb290IG9yZyB0ZXN0IHNlcnZlcjCCAaIwDQYJKoZI
+hvcNAQEBBQADggGPADCCAYoCggGBALr4XBUbhb0yXlnErnOisGcGB2nmiARNmEVd
+tkN4ix9TsyIK0bJIeJY4uJgaCEESKjjMOq64nXPqfLp6COq8JPObH4HrVG7ZFrIV
+vGY2eEMMBo66qrb0+xLUHwQis1sDj7emJd9YxMqzCcRUsqQl+q+mJNiDnxj8CGza
+9K5oV3XWqMG/2zn3sMJjjZUYAQrugdDidtVSXCX4qgyofzTK4rwONL5jf0oBkDZc
+TdnZ7gX84sihMnbcOVQz3cS4HqH1I8F/O2r/hJhgH1OGv6zWTliakaoTHqWeYeu6
+jDXaiU1Ie4LPldRo/SSqQ5w6W4b2fS1B3GM2LMLFC0kERvPEueAZ5AaATJ+fGmua
+iEJsbKlIHJ3/oXEsRFrlP7S8tkPbRx9lFUE/0HqPwRzwkxHHE3RffEfpLLzQevzA
+iW7pHoKF76evV9P8r8eaNAZgLtu8AdUIY44HJ1E6FwpxIlbr6a/2SDFChy2VBa34
+xna8Fy9QR2iVSmtNzrpRKx7hs9V5JwIDAQABo4GNMIGKMAwGA1UdEwEB/wQCMAAw
+FAYDVR0RBA0wC4IJbG9jYWxob3N0MBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud
+DwEB/wQFAwMHoAAwHQYDVR0OBBYEFPqxluDqr49IyPT+0HyX5Kq7o6HtMB8GA1Ud
+IwQYMBaAFDAX4bzP3gaOQYZyZC0eGBqQmIJkMA0GCSqGSIb3DQEBCwUAA4IBgQCA
+K1GVJ9NhEbkH4vbZrRtUwyIWJgFbUXy3TzXIRZTUMgMzEZXDyVkElOcGPxev6kMt
+TdSRZrc2DteAnWpJ6fSUt3hU13O7r7H8R7jF3UvqotgiQi+lOvUKPKUiU3ecuClO
+NBFE7bjiTOtMyiGWjsVcBb3aHcPjEWm8pTPW3yuSXdg0J5pBjjxqK8m3ExMPK3P+
+sBE7eSTuHiLBWAIRgM1I/F5COV7QKX1CM2COmJWEDS1t3qORKZPzNWuaVXwtqbbA
+qC4OIpT8DakSLWyPK74vLx1yoUJu5wtoifHG9nnrFvstLE4DNLiB/fN08FI1ka13
+hJyJMQecl84kunX1hkwP/o4IrZ/pSWk2d9PCm35Td/g7jaI+06IqnTIyQblXRYIQ
+iPfmCWEyFBwFJQVND2c0JJfNeu16qVniarwnXa8z6j5eBI3E94weH0ZOdOSKD//c
+iQvU7JpoC9Gc+jJFJqVE5RE7ueZRqN+7dJbe4CyrAhm2U0zwel9XNZtWaHMHT7Y=
+-----END CERTIFICATE-----
diff --git a/daemon/lua/trust_anchors.test/x509/server.tmpl b/daemon/lua/trust_anchors.test/x509/server.tmpl
new file mode 100644
index 0000000..7ee40d2
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/server.tmpl
@@ -0,0 +1,6 @@
+organization = Fake DNS root org test server
+cn = localhost
+tls_www_server
+encryption_key
+signing_key
+dns_name = localhost
diff --git a/daemon/lua/trust_anchors.test/x509/wrongca-key.pem b/daemon/lua/trust_anchors.test/x509/wrongca-key.pem
new file mode 100644
index 0000000..56b69fe
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/wrongca-key.pem
@@ -0,0 +1,182 @@
+Public Key Info:
+ Public Key Algorithm: RSA
+ Key Security Level: High (3072 bits)
+
+modulus:
+ 00:e4:cc:5d:b2:a0:a5:0e:94:f2:d1:ca:cd:04:36:72
+ 95:9a:61:ac:c7:d6:35:24:63:d6:3b:2d:38:4b:a5:69
+ fe:91:f5:d8:a6:db:df:9a:17:fd:21:5d:bc:94:9f:3b
+ cf:f7:fa:9d:1d:7f:22:0f:90:36:91:4f:3a:92:77:55
+ 6e:33:e6:c9:94:a6:a1:c7:e6:14:f4:3d:27:18:32:5c
+ 0c:9a:5d:5a:8a:77:45:50:6e:00:e7:c5:14:cf:4f:75
+ 39:60:99:5b:5d:78:61:f1:5e:cb:6e:78:81:fb:fe:ad
+ dc:50:89:ce:b6:4d:78:79:e2:84:1c:c1:be:72:ce:d5
+ 7c:66:82:46:bb:da:ba:cc:69:4c:66:dc:a9:86:56:c3
+ 0e:bc:a4:b9:85:0b:c9:07:6f:a4:a7:24:33:ee:bc:02
+ 9d:73:61:82:e0:f0:dd:a6:e9:24:a0:ca:db:14:09:79
+ 9b:66:6f:2b:4f:19:b0:d3:44:85:04:c3:e6:b6:38:a0
+ 57:8b:bb:b2:95:03:ab:45:97:9c:fd:55:fd:bc:84:64
+ 9b:f1:f0:6a:c7:be:e3:3d:1c:ba:7b:b9:b9:3d:96:ca
+ ad:ac:c3:dc:69:80:ba:27:1a:91:73:61:11:64:09:7b
+ e6:c7:ac:ef:16:66:2f:c1:dc:8e:8c:4b:23:8f:14:39
+ 01:20:2b:97:fd:30:72:bd:12:e1:e1:4b:b7:2f:ec:08
+ 47:4b:7c:ec:8f:2f:b1:2b:5d:b0:af:fe:f0:0e:8e:5e
+ 90:87:53:3f:f9:4f:fe:cf:9e:6a:ac:41:4e:30:be:93
+ 5c:2b:7e:d3:14:c6:a6:23:ac:aa:4c:88:73:f9:79:77
+ df:e3:66:0a:9b:fa:b2:7e:1d:f4:23:1d:e9:76:48:2e
+ 48:34:0d:92:7d:30:05:ea:00:6e:57:cd:23:49:2b:35
+ d0:08:6d:84:ab:9d:6f:54:92:16:8e:cb:c5:25:76:96
+ 02:c3:9f:89:33:4c:a3:ee:d4:ee:cc:53:bc:a3:a8:30
+ a7:
+
+public exponent:
+ 01:00:01:
+
+private exponent:
+ 42:04:6d:ec:c2:c9:9d:81:80:e6:e3:db:70:21:bd:c2
+ 48:ff:71:f9:5f:67:8a:0e:7c:9c:2a:9a:19:c9:aa:e1
+ d7:7f:d6:79:9e:eb:cf:ec:a8:0f:5e:9a:b1:4f:98:d4
+ 93:3a:ee:e6:b9:ee:3c:0b:62:93:5f:07:09:88:01:50
+ 81:0d:50:90:e1:db:c0:70:35:0f:9c:2d:91:9f:c6:4b
+ a4:a6:d7:1f:28:f7:09:14:14:92:cb:9d:0b:8c:63:c8
+ a0:84:df:86:02:dc:4f:e7:08:4f:e1:d9:af:ba:76:b4
+ 21:51:02:22:3c:0c:4d:2e:fc:eb:c7:43:8a:a0:ff:9c
+ 9b:7f:6f:a5:78:a1:79:a5:d0:73:c5:ab:ec:a5:50:37
+ 0a:5b:85:64:4c:58:3f:1b:09:a9:68:9a:cb:81:d8:47
+ 52:29:8a:b8:19:07:fb:c7:3d:d8:5f:70:03:42:10:a2
+ b4:55:d3:6b:49:80:6a:15:55:44:60:ba:4b:4d:df:85
+ 97:7c:47:4f:3e:8c:46:ed:72:7b:d8:63:5d:4d:05:96
+ f8:47:e4:65:c9:18:8f:13:e4:36:24:cf:c7:fe:3e:2e
+ 9c:7f:09:5a:0a:3d:0a:6a:f7:e6:27:0b:ed:8c:fc:db
+ 9c:c3:77:fd:6d:f2:d5:ab:c3:0e:3a:cb:49:95:8a:f3
+ 9b:ab:3e:2c:d2:82:9a:61:f0:2c:2c:f7:37:ad:f5:32
+ ee:96:f8:cc:a4:a1:97:19:45:fc:8c:82:33:ca:73:86
+ 7e:79:94:fe:6a:a0:53:bf:8b:7d:b0:da:1c:a4:f4:7f
+ a1:d9:4f:76:75:20:35:b4:69:9c:c8:23:7e:07:b9:81
+ 4e:04:81:8e:88:d5:83:51:6e:cc:f2:6b:e9:af:ad:bc
+ 42:f9:2c:8a:c3:54:ce:f1:1a:bc:2c:8f:99:a0:a6:0a
+ cc:92:5b:6d:78:c7:c0:cc:7d:eb:42:eb:c3:69:46:0b
+ 33:d1:3e:c8:1a:13:e8:db:ee:ea:d4:91:77:0b:44:51
+
+
+prime1:
+ 00:e9:63:e7:aa:1a:2e:41:51:17:10:33:53:de:95:f1
+ 7a:8e:55:57:c5:f8:a4:25:c3:e1:83:fe:ad:68:fe:d3
+ 86:10:f7:f0:9a:d2:69:dc:15:bc:0c:e4:44:d3:98:0a
+ 0d:71:77:e6:53:36:57:a2:2e:22:b1:e7:8d:4a:64:33
+ 0f:5c:11:20:bb:ae:21:46:d4:82:35:b1:9a:95:96:22
+ 50:c6:6c:8c:e5:55:93:d0:87:9a:3a:6a:3f:13:8a:3b
+ cf:0e:04:38:b5:19:2e:95:d4:d8:2d:80:36:ac:18:63
+ 74:ad:ea:eb:20:f5:9a:13:1e:82:c2:10:9f:78:12:f8
+ 74:af:a4:ee:29:bb:da:d9:a4:fa:a7:25:4f:ae:52:3e
+ 92:02:dd:f6:65:28:0c:27:36:d1:3d:a2:7c:c7:01:55
+ 5c:c1:88:d4:3e:fd:92:70:4a:15:43:0a:5b:ec:37:ea
+ 12:42:db:7c:45:c6:2f:bc:ac:a7:c7:f2:4b:4a:b0:3c
+ 63:
+
+prime2:
+ 00:fa:f6:94:95:53:f4:c7:7e:93:e1:3c:c6:6f:97:d2
+ a0:52:44:04:56:18:40:fb:f0:c0:5b:00:cb:45:71:c9
+ 54:c9:e2:50:91:77:ee:60:f3:33:fc:c5:7e:fc:07:7e
+ 06:5e:5e:8e:db:18:72:83:9c:00:02:d0:a0:86:5c:9f
+ 80:72:37:16:3e:39:3f:b7:1c:08:c8:79:ed:51:f2:4d
+ 61:3a:48:4b:09:7d:a5:f4:7a:8e:b4:5c:cc:0d:56:66
+ 98:2e:fc:2f:26:1c:75:6f:a4:7d:2b:7d:f9:5c:dc:52
+ a9:51:f9:26:b0:94:71:5e:d2:8e:85:38:02:57:9b:31
+ 74:40:7b:81:65:ad:56:5a:b4:ac:b8:40:80:7d:b2:a2
+ 5b:81:9b:b3:c2:41:de:7d:f6:66:b0:a2:a7:13:11:a6
+ 07:51:6f:e9:79:97:43:67:dd:8f:a2:c3:e3:5b:74:50
+ d9:c2:e7:cf:2d:5b:9e:bf:01:c0:77:cf:9c:06:22:63
+ ed:
+
+coefficient:
+ 00:83:36:59:9d:73:95:c9:07:11:60:b5:0f:1e:8d:81
+ ee:78:9e:92:50:4c:e7:00:a6:21:8a:e8:fd:12:de:6c
+ 2d:b3:99:53:90:79:74:e2:75:7b:f8:d9:d1:c5:29:28
+ 9e:bc:5a:ad:28:7c:8b:47:14:17:14:a6:08:e6:ec:9d
+ dd:74:9b:d5:bb:c6:54:24:25:81:a2:b7:b6:8a:bd:e4
+ da:f5:7e:08:4a:cd:f2:7c:2a:cc:bf:ed:c3:d4:66:05
+ 3f:f8:39:07:77:e6:13:cf:88:28:d5:1e:2a:4b:13:66
+ 59:39:ef:7b:3d:43:fa:77:9f:64:81:45:52:da:03:b0
+ 2b:04:46:f1:79:a0:bc:08:b1:9b:0c:ae:91:4b:96:08
+ ad:26:a0:a6:f1:c4:d5:74:c6:f8:4a:7c:01:5c:12:99
+ 1e:58:92:b9:e5:13:20:0b:fc:b4:2a:af:00:00:a3:22
+ 24:1c:f8:f9:67:ec:37:c0:fd:22:c7:97:cb:4f:85:e5
+ fb:
+
+exp1:
+ 07:0a:5d:7d:b3:26:7c:0f:ef:2b:2c:f1:35:c0:be:35
+ 1d:40:13:d2:c4:0a:67:9d:3e:1b:56:3e:72:f1:64:fe
+ 21:5a:e9:66:32:3c:c1:47:e2:91:5c:fd:7a:88:96:9d
+ 0b:34:3b:bd:7c:e1:2d:e2:48:67:a7:7d:8a:a5:f5:28
+ 5c:75:a3:d0:25:93:99:68:65:b9:2a:ef:67:dd:cc:91
+ 35:3b:27:10:f0:00:f2:84:74:b1:98:6c:e8:b0:fd:d6
+ e4:2c:5a:6d:94:21:e4:a1:34:18:43:4a:e2:ec:25:6b
+ ea:a7:30:8a:a0:fe:11:df:94:c0:37:f2:27:94:22:ec
+ 9a:33:d6:7f:69:cc:53:4e:77:c5:3d:09:e7:4f:51:d5
+ e3:c2:40:61:92:d0:b3:0a:23:4b:c1:b0:13:ec:c5:5f
+ 73:f3:25:bb:f4:b8:4b:2c:e0:f1:51:c9:ae:19:8d:b5
+ 19:51:37:b1:7f:26:07:82:09:d9:ad:44:7a:2f:50:d3
+
+
+exp2:
+ 54:61:21:d6:0f:73:66:bb:ed:56:b6:cb:75:6b:d3:9a
+ a9:ee:4a:92:f3:f6:ad:7e:e5:fd:f3:07:65:62:fb:b1
+ 71:6b:91:71:47:a4:b3:9e:31:e5:94:35:bc:e1:7c:03
+ 02:29:c2:d8:71:a0:d7:15:55:7f:9c:cb:cc:41:4a:33
+ b2:b3:48:dc:44:fd:62:40:9f:c7:60:0f:66:15:14:e5
+ 52:e6:49:ac:78:3d:9b:34:b5:d4:78:ba:f6:e5:0c:fb
+ b0:18:84:75:c8:ed:c7:4a:c3:f7:22:94:fc:1e:ec:00
+ 18:1a:b0:62:80:96:99:ae:2b:d3:28:e0:c8:b9:da:67
+ de:e2:67:c0:5b:06:84:da:e8:93:ce:c4:24:ff:31:cd
+ 98:87:54:6a:45:21:5c:b2:c3:16:32:aa:00:24:57:f1
+ 6f:f5:33:c7:f9:0e:e5:d6:3d:dc:19:06:d1:92:0b:39
+ 1c:6a:3a:63:62:c1:be:31:05:98:83:0a:4f:99:b2:85
+
+
+
+Public Key PIN:
+ pin-sha256:75s1y3dUDBiOALg1xQKcI2/wXlZsFuALytI0Khf7WYQ=
+Public Key ID:
+ sha256:ef9b35cb77540c188e00b835c5029c236ff05e566c16e00bcad2342a17fb5984
+ sha1:f91b5944557c4e3bb4cee62a6a2a525b950bbd99
+
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/daemon/lua/trust_anchors.test/x509/wrongca.pem b/daemon/lua/trust_anchors.test/x509/wrongca.pem
new file mode 100644
index 0000000..2b536b9
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/wrongca.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEDzCCAnegAwIBAgIUSDi0iXY6awSZoHpkeTMf8gD8u8UwDQYJKoZIhvcNAQEL
+BQAwHzEdMBsGA1UEAxMUQW5vdGhlciB1bnJlbGF0ZWQgQ0EwHhcNMTkwMTAzMTYy
+NzMzWhcNMjAwMTAzMTYyNzMzWjAfMR0wGwYDVQQDExRBbm90aGVyIHVucmVsYXRl
+ZCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAOTMXbKgpQ6U8tHK
+zQQ2cpWaYazH1jUkY9Y7LThLpWn+kfXYptvfmhf9IV28lJ87z/f6nR1/Ig+QNpFP
+OpJ3VW4z5smUpqHH5hT0PScYMlwMml1aindFUG4A58UUz091OWCZW114YfFey254
+gfv+rdxQic62TXh54oQcwb5yztV8ZoJGu9q6zGlMZtyphlbDDrykuYULyQdvpKck
+M+68Ap1zYYLg8N2m6SSgytsUCXmbZm8rTxmw00SFBMPmtjigV4u7spUDq0WXnP1V
+/byEZJvx8GrHvuM9HLp7ubk9lsqtrMPcaYC6JxqRc2ERZAl75ses7xZmL8HcjoxL
+I48UOQEgK5f9MHK9EuHhS7cv7AhHS3zsjy+xK12wr/7wDo5ekIdTP/lP/s+eaqxB
+TjC+k1wrftMUxqYjrKpMiHP5eXff42YKm/qyfh30Ix3pdkguSDQNkn0wBeoAblfN
+I0krNdAIbYSrnW9UkhaOy8UldpYCw5+JM0yj7tTuzFO8o6gwpwIDAQABo0MwQTAP
+BgNVHRMBAf8EBTADAQH/MA8GA1UdDwEB/wQFAwMHBAAwHQYDVR0OBBYEFPkbWURV
+fE47tM7mKmoqUluVC72ZMA0GCSqGSIb3DQEBCwUAA4IBgQB7Nbn5Pc4eX8XuVnk+
+RzkB1XZ8UubdQKBUGZ2t4TFZDBuu6MZDhgyyJUU2rD/ndp+9No0BLCEB/qhL5p5W
+SEsSb0t0jmJFmCkHdBefUPqhOW7k7ttuEGN8ctWXloczFrV1dJNR1gfcb6guhIGu
+Q5k+xM0x69VbM0iTMaXqEtAYQqzmDsEpRep6lUbd9g7MLhRE9Y5NWms4EzLlKypR
+z0TSdSFPS/Fz//bzEct/e+Wp8ss42/0JljQMIRYYD7i+x6azF1brhvnadfNmOVFE
+1iN0SnHsPeuD3+v7eji389tnRF3xjKO1Vx8BUVPkQqAWt13xxDqhV41MjyZwFynB
+aG+zGYZ0DyTE/8/ouN5+SN0eMwW/oFSckvFIvCXqS9qf3YuMo+e0eJ/xKozejg+r
+DPUVGmKEFfEZYWjxl3ZVrM4tfSkjzwRKNPZ1NaXcDU3/fbfWtR2cGnNi60AquENA
+GUxwKfDZA3n8dOJQwFRnGXiBNT7nwZZXdow00kGuN/0BZ6Q=
+-----END CERTIFICATE-----
diff --git a/daemon/lua/trust_anchors.test/x509/wrongca.tmpl b/daemon/lua/trust_anchors.test/x509/wrongca.tmpl
new file mode 100644
index 0000000..81dfbfc
--- /dev/null
+++ b/daemon/lua/trust_anchors.test/x509/wrongca.tmpl
@@ -0,0 +1,3 @@
+cn = Another unrelated CA
+ca
+cert_signing_key
diff --git a/daemon/lua/zonefile.lua b/daemon/lua/zonefile.lua
new file mode 100644
index 0000000..fdfc211
--- /dev/null
+++ b/daemon/lua/zonefile.lua
@@ -0,0 +1,91 @@
+-- LuaJIT ffi bindings for zscanner, a DNS zone parser.
+-- Author: Marek Vavrusa <marek.vavrusa@nic.cz>
+
+local ffi = require('ffi')
+local libzscanner = ffi.load(libzscanner_SONAME)
+
+-- Wrap scanner context
+local zs_scanner_t = ffi.typeof('zs_scanner_t')
+ffi.metatype( zs_scanner_t, {
+ __gc = function(zs) return libzscanner.zs_deinit(zs) end,
+ __new = function(ct, origin, class, ttl)
+ if not class then class = 1 end
+ if not ttl then ttl = 3600 end
+ local parser = ffi.new(ct)
+ libzscanner.zs_init(parser, origin, class, ttl)
+ return parser
+ end,
+ __index = {
+ open = function (zs, file)
+ assert(ffi.istype(zs, zs_scanner_t))
+ local ret = libzscanner.zs_set_input_file(zs, file)
+ if ret ~= 0 then return false, zs:strerr() end
+ return true
+ end,
+ parse = function(zs, input)
+ assert(ffi.istype(zs, zs_scanner_t))
+ if input ~= nil then libzscanner.zs_set_input_string(zs, input, #input) end
+ local ret = libzscanner.zs_parse_record(zs)
+ -- Return current state only when parsed correctly, otherwise return error
+ if ret == 0 and zs.state ~= "ZS_STATE_ERROR" then
+ return zs.state == "ZS_STATE_DATA"
+ else
+ return false, zs:strerr()
+ end
+ end,
+ current_rr = function(zs)
+ assert(ffi.istype(zs, zs_scanner_t))
+ return {
+ owner = ffi.string(zs.r_owner, zs.r_owner_length),
+ ttl = tonumber(zs.r_ttl),
+ class = tonumber(zs.r_class),
+ type = tonumber(zs.r_type),
+ rdata = ffi.string(zs.r_data, zs.r_data_length),
+ comment = zs:current_comment(),
+ }
+ end,
+ strerr = function(zs)
+ assert(ffi.istype(zs, zs_scanner_t))
+ return ffi.string(libzscanner.zs_strerror(zs.error.code))
+ end,
+ current_comment = function(zs)
+ if zs.buffer_length > 0 then
+ return ffi.string(zs.buffer, zs.buffer_length - 1)
+ else
+ return nil
+ end
+ end
+ },
+})
+
+-- Module API
+local rrparser = {
+ new = zs_scanner_t,
+
+ -- Parse a file into a list of RRs
+ file = function (path)
+ local zs = zs_scanner_t()
+ local ok, err = zs:open(path)
+ if not ok then
+ return ok, err
+ end
+ local results = {}
+ while zs:parse() do
+ table.insert(results, zs:current_rr())
+ end
+ return results
+ end,
+
+ -- Parse a string into a list of RRs.
+ string = function (input)
+ local zs = zs_scanner_t()
+ local results = {}
+ local ok = zs:parse(input)
+ while ok do
+ table.insert(results, zs:current_rr())
+ ok = zs:parse()
+ end
+ return results
+ end,
+}
+return rrparser