diff options
-rw-r--r-- | c_src/dnssec.c | 41 | ||||
-rw-r--r-- | src/dns.erl | 93 | ||||
-rw-r--r-- | src/dnssecport.erl | 92 | ||||
-rw-r--r-- | src/v1.erl | 26 |
4 files changed, 174 insertions, 78 deletions
diff --git a/c_src/dnssec.c b/c_src/dnssec.c index 6b9431d..440353b 100644 --- a/c_src/dnssec.c +++ b/c_src/dnssec.c @@ -4,23 +4,25 @@ * * Invocation: dnssec <path-to-trust-anchor-file> * - * Once running, read DNSSEC RR's from stdin, canonicalise RR's - * (RFC4034 6.2), validate RR's (todo:ref) and write the result to + * Once running: Read DNSSEC RR's from stdin, canonicalise RR's + * (RFC4034 6.2), validate DS RR (todo:ref) and write the result to * stdout. * * All length fields in the input and output denotes the length of the - * piece of data to follow in number of octets. + * piece of data to follow in number of octets. All integers are + * transfered in network byte order (a.k.a. big-endian). * * Input format: - * - Length of data (4 octets) + * - Length of data in number of octets (integer, 4 octets) + * - Validation time in seconds since the epoch (integer, 4 octets) + * - Validation time skew in seconds (integer, 4 octets) * - DNSSEC RR's as a DNSSEC_key_chain, specified in * draft-zhang-trans-ct-dnssec-03 section 4.1 but without the TLS * data structure encoding. * * Output format: - * - Lenght of data (4 octets) - * - Status code -- the getdns_return_t value in network byte order (2 - * octets) + * - Lenght of data (integer, 4 octets) + * - Status code -- the getdns_return_t value (integer, 2 octets) * - (RR's)* -- if validation succeeded: the DS+RRSIG and the full * chain up to and including the trust anchor; if validation failed: * nothing @@ -33,7 +35,7 @@ #include <string.h> #include <errno.h> #include <time.h> -#include <endian.h> +#include <arpa/inet.h> #include <getopt.h> #include <getdns/getdns.h> #include <getdns/getdns_extra.h> @@ -42,6 +44,16 @@ static int debug = 0; /* DEBUG */ +#define hd(b, l) { \ + for (size_t n = 0; n < (l); n++) { \ + if (n % 16 == 0) { \ + if (n != 0) fprintf(stderr, "\n"); \ + fprintf(stderr, "%08x ", n); \ + } else if (n % 8 == 0) { \ + fprintf(stderr, " "); } \ + fprintf(stderr, "%02hhx ", (b)[n]); } \ + fprintf(stderr, "\n"); } + #if defined(TEST) static char *testmode = NULL; #endif @@ -272,8 +284,6 @@ out: } #endif /* !TEST */ -#define DNSSEC_VALIDATION_SKEW 30 /* Seconds. */ - static void loop(getdns_list *trust_anchors) { @@ -287,8 +297,13 @@ loop(getdns_list *trust_anchors) size_t out_len = 0; #if !defined(TEST) - r = validate(buf, len, trust_anchors, - time(NULL), DNSSEC_VALIDATION_SKEW, + unsigned char *bufp = buf; + uint32_t validation_time = ntohl(*((uint32_t *)bufp)); + bufp += 4; + uint32_t validation_time_skew = ntohl(*((uint32_t *)bufp)); + bufp += 4; + r = validate(bufp, len - 8, trust_anchors, + validation_time, validation_time_skew, reply + 2, &out_len); #else r = test_validate(buf, len, trust_anchors, testmode); @@ -308,7 +323,7 @@ loop(getdns_list *trust_anchors) } } - *((uint16_t *) reply) = htobe16(r); + *((uint16_t *) reply) = htons(r); if (debug) fprintf(stderr, "writing %d octets of data, including status code %d\n", 2 + out_len, r); diff --git a/src/dns.erl b/src/dns.erl index b8a8ffe..f327a8f 100644 --- a/src/dns.erl +++ b/src/dns.erl @@ -3,10 +3,10 @@ -module(dns). -export([decode_rrset/1, decode_rr/1, encode_rrset/1, encode_rr/1, - canonicalize_dsrr/2]). + canonicalize/1, validate/1]). -record(rr, {name :: list(), % List of name labels. - type :: binary(), + type :: non_neg_integer(), class :: binary(), ttl :: integer(), rdata :: binary()}). @@ -14,8 +14,15 @@ -spec decode_name_label(binary()) -> tuple(). decode_name_label(RRbin) -> - <<Len:8/integer, Label:Len/binary, Rest/binary>> = RRbin, - {binary_to_list(Label), Rest}. + <<IsPtr:2/integer, _:6/integer, _/binary>> = RRbin, + case IsPtr of + 0 -> + <<_:2/integer, Len:6/integer, Label:Len/binary, Rest/binary>> = RRbin, + {binary_to_list(Label), Rest}; + _ -> + <<_:2/integer, _Ptr:14/integer, Rest/binary>> = RRbin, + {'*compressed*', Rest} + end. -spec encode_name_label(string()) -> binary(). encode_name_label(Label) -> @@ -40,10 +47,17 @@ encode_name([], Acc) -> encode_name([H|T], Acc) -> encode_name(T, [encode_name_label(H) | Acc]). +has_compressed_name_p(Name) -> + lists:any(fun(Label) -> case Label of + '*compressed' -> true; + _ -> false + end + end, Name). + -spec decode_rr(binary()) -> {rr(), binary()}. decode_rr(RRBin) -> {Name, RestRR} = decode_name(RRBin), - <<Type:2/binary, + <<Type:2/integer-unit:8, Class:2/binary, TTL:4/integer-unit:8, RDLength:2/integer-unit:8, @@ -66,7 +80,7 @@ encode_rr(#rr{name = Name, type = Type, class = Class, ttl = TTL, rdata = RDATA} EncodedName = encode_name(Name), RDLength = byte_size(RDATA), <<EncodedName/binary, - Type:2/binary, + Type:2/integer-unit:8, Class:2/binary, TTL:4/integer-unit:8, RDLength:2/integer-unit:8, @@ -80,20 +94,59 @@ encode_rrset([], Acc) -> encode_rrset([H|T], Acc) -> encode_rrset(T, [encode_rr(H) | Acc]). -%% Cacnonicalise a single DS RR according to RFC4034 section 6.2. -canonicalize_dsrr(DS, RRSIG) -> - %% 1. expand domain name - %% FIXME: What does a compressed name look like? - - %% 2. lowercase - LCName = lists:map(fun(L) -> string:to_lower(L) end, DS#rr.name), - - %% 3. N/A for DS - %% 4. N/A for DS FIXME: verify - - %% 5. set TTL to that of the RRSIG - OrigTTL = RRSIG#rr.ttl, +%% Canonicalise a single RR according to RFC4034 section 6.2. +canonicalize_rr_form(RR, RRSIG) -> + %% 1. Expand domain name -- a label with a length field >= 0xC0 is + %% a two octet pointer, which we can't expand (since we don't have + %% the full message): Do nothing. + + %% 2. Owner name casing: Lowercase. + LCName = lists:map(fun string:to_lower/1, RR#rr.name), + + %% 3. DNS names in RDATA casing -- N/A for DS and DNSKEY but + %% FIXME: needs to be done for RRSIG? + + %% 4. FIXME: unexpanded owner name + + %% 5. Set TTL to "Original TTL" of the corresponding RRSIG. + <<_:4/binary, OrigTTL:32/integer, _/binary>> = RRSIG#rr.rdata, + + RR#rr{name = LCName, ttl = OrigTTL}. + +%% Canonicalise an RRset with DNSKEY, DS, and RRSIG records according +%% to RFC4034 section 6. Records of other types are removed. Duplicate +%% records are removed. +canonicalize(RRset) -> + %% 6.1 owner name order + RRset61 = RRset, % TODO + + %% 6.2 RR form + [DS, RRSIG | Rest] = RRset61, + C14N_DS = canonicalize_rr_form(DS, RRSIG), + RRset62 = [C14N_DS, RRSIG | Rest], + + %% 6.3 RR ordering (and dropping duplicates) + RRset63 = RRset62, + + RRset63. + +%% Is the RR set valid for our needs from a DNS point of view? If so, +%% return the signature inception time of the RRSIG covering the DS +%% RR, to be used as the validation time for the DNSSEC validation. +-spec validate(binary()) -> {valid, integer()} | {invalid, atom()}. +validate(RRsetBin) -> + [DS, RRSIG | _Rest] = decode_rrset(RRsetBin), + case has_compressed_name_p(DS#rr.name) of + false when DS#rr.type == 43, + RRSIG#rr.type == 46 -> + <<_:12/binary, SigInceptionTime:32/integer, _/binary>> = + RRSIG#rr.rdata, + {valid, SigInceptionTime}; + false -> + {invalid, badtype}; + true -> + {invalid, compressed_name} + end. - DS#rr{name = LCName, ttl = OrigTTL}. %% TODO: Add unit tests. diff --git a/src/dnssecport.erl b/src/dnssecport.erl index e9c3345..39ce230 100644 --- a/src/dnssecport.erl +++ b/src/dnssecport.erl @@ -39,37 +39,22 @@ decode_response(Response) -> <<Status:16/integer, RRSet/binary>> = Response, {ok, Status, dns:decode_rrset(RRSet)}. +-define(DNSSEC_VALIDATION_TIME_SKEW, 30). handle_call(stop, _From, State) -> lager:debug("dnssec stop request received"), stop_port(State); -handle_call({validate, Data}, _From, State) -> - case State#state.port of - undefined -> - {reply, {error, noport}, State}; - Port when is_port(Port) -> - Port ! {self(), {command, Data}}, - receive - {Port, {data, Response}} -> - case decode_response(list_to_binary(Response)) of - {ok, 400, [DS | Chain]} -> - RRSIG = hd(Chain), - R = [dns:encode_rr(dns:canonicalize_dsrr(DS, RRSIG)), - dns:encode_rrset(Chain)], - {reply, {ok, R}, State}; - {ok, Error, _} -> - lager:debug("DNSSEC validation failed with ~p", - [Error]), - {reply, {error, Error}, State} - end; - {Port, {exit_status, ExitStatus}} -> - lager:error("dnssec port ~p exiting with status ~p", - [Port, ExitStatus]), - {stop, portexit, State#state{port = undefined}} - after - 3000 -> - lager:error("dnssec port timeout"), - {stop, timeout, State} - end +handle_call({validate, RRset}, _From, State) -> + case dns:validate(RRset) of + {valid, ValidationTime} -> + case State#state.port of + undefined -> + {reply, {error, noport}, State}; + Port when is_port(Port) -> + portcommand(RRset, ValidationTime, + ?DNSSEC_VALIDATION_TIME_SKEW, State) + end; + {invalid, Reason} -> + {reply, {invalid, Reason}, State} end. handle_info(_Info, State) -> @@ -86,6 +71,34 @@ terminate(Reason, _State) -> ok. %%%%%%%%%%%%%%%%%%%% +-spec portcommand(binary(), integer(), integer(), #state{}) -> + {stop, portexit|timeout, #state{}} | + {reply, tuple(), #state{}}. +portcommand(Data, ValidationTime, Skew, State) -> + Port = State#state.port, + Port ! {self(), {command, + <<ValidationTime:32/integer, + Skew:32/integer, + Data/binary>>}}, + receive + {Port, {data, Response}} -> + case decode_response(list_to_binary(Response)) of + {ok, 400, RRset} -> + C14N_RRset = dns:canonicalize(RRset), + {reply, {valid, dns:encode_rrset(C14N_RRset)}, State}; + {ok, Code, _} -> + {reply, {invalid, Code}, State} + end; + {Port, {exit_status, ExitStatus}} -> + lager:error("dnssec port ~p exiting with status ~p", + [Port, ExitStatus]), + {stop, portexit, State#state{port = undefined}} + after + 3000 -> + lager:error("dnssec port timeout"), + {stop, timeout, State} + end. + create_port(Program, Args) -> open_port({spawn_executable, Program}, [{args, Args}, @@ -113,8 +126,8 @@ stop_port(State) -> -define(REQ1_FILE, "test/testdata/dnssec/testrrsets/req-basic"). -define(REQ2_FILE, "test/testdata/dnssec/testrrsets/req-lowttl"). -start_test_port() -> - create_port("priv/dnssecport", [?TA_FILE]). +start_test_port(Args) -> + create_port("priv/dnssecport", Args). stop_test_port(Port) -> {stop, closed, _State} = stop_port(#state{port = Port}), @@ -133,7 +146,7 @@ read_dec_enc_test_() -> full_test_() -> {setup, fun() -> - start_test_port() end, + start_test_port([?TA_FILE]) end, fun(Port) -> stop_test_port(Port) end, fun(Port) -> @@ -141,15 +154,24 @@ full_test_() -> self(), #state{port = Port}), R2 = handle_call({validate, read_submission_from_file(?REQ2_FILE)}, self(), #state{port = Port}), - {reply, {ok, [DSBin | _ChainBin]}, _} = R2, - {DS, <<>>} = dns:decode_rr(DSBin), + {reply, {valid, ChainBin}, _} = R2, + {DS, _} = dns:decode_rr(ChainBin), [ - ?_assertMatch({reply, {ok, _}, _State}, R1), - ?_assertMatch({reply, {ok, _}, _State}, R2), + ?_assertMatch({reply, {valid, _}, _State}, R1), + ?_assertMatch({reply, {valid, _}, _State}, R2), ?_assertMatch({rr, _Name, _Type, _Class, 3600, _RDATA}, DS) ] end }. +no_trust_anchors_test_() -> + {setup, + fun() -> start_test_port([]) end, + fun(Port) -> stop_test_port(Port) end, + fun(Port) -> + R = handle_call({validate, read_submission_from_file(?REQ1_FILE)}, + self(), #state{port = Port}), + [?_assertMatch({reply, {invalid, 401}, _}, R)] end}. + %% start_test_port(TestType) -> %% Port = create_port("priv/dnssecport", ["--testmode", atom_to_list(TestType)]), %% ?debugFmt("Port: ~p", [Port]), @@ -157,17 +157,23 @@ add_rr_chain(Input) -> {'EXIT', _} -> err400("add-rr-chain: invalid base64-encoding:", B64); Data -> - case dnssecport:validate(Data) of - {ok, [DS | Chain]} -> - lager:debug("succesful DNSSEC validation"), - success(catlfish:add_chain(DS, Chain, normal)); - {error, ErrorCode} -> - err400(io_lib:format( - "add-rr-chain: invalid DS record: ~p", - [ErrorCode]), - Data) - end + add_chain_helper(Data) end; _ -> err400("add-rr-chain: missing input: chain", Input) end. + +add_chain_helper(Data) -> + case dnssecport:validate(Data) of + {valid, [DS | Chain]} -> + lager:debug("succesful DNSSEC validation"), + success(catlfish:add_chain(DS, Chain, normal)); + {invalid, Reason} -> + lager:debug("DNSSEC validation failed with ~p", [Reason]), + err400(io_lib:format("add-rr-chain: invalid DS record: ~p", + [Reason]), Data); + {error, Reason} -> + lager:debug("DNSSEC validation error: ~p", [Reason]), + err400(io_lib:format("add-rr-chain: unable to validate record: ~p", + [Reason]), Data) + end. |