diff options
author | Linus Nordberg <linus@nordu.net> | 2016-04-13 10:57:23 +0200 |
---|---|---|
committer | Linus Nordberg <linus@nordu.net> | 2016-04-13 10:57:23 +0200 |
commit | 49d8ed9587b1363f2feddc39f31442fd292798f2 (patch) | |
tree | b761d6a9aa998b5b93a1053c10134cd13a09f16f /src | |
parent | fc16553ab4f5f956de7e4633d7dc92ea20c118e3 (diff) |
DNSSEC validation improvements.
Use DS signature inception time as the DNSSEC validation time.
Validate input data a bit more.
Set TTL in DS to "Original TTL" of RRSIG (this time for real).
Diffstat (limited to 'src')
-rw-r--r-- | src/dns.erl | 93 | ||||
-rw-r--r-- | src/dnssecport.erl | 92 | ||||
-rw-r--r-- | src/v1.erl | 26 |
3 files changed, 146 insertions, 65 deletions
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. |