summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLinus Nordberg <linus@nordu.net>2016-04-13 10:57:23 +0200
committerLinus Nordberg <linus@nordu.net>2016-04-13 10:57:23 +0200
commit49d8ed9587b1363f2feddc39f31442fd292798f2 (patch)
treeb761d6a9aa998b5b93a1053c10134cd13a09f16f /src
parentfc16553ab4f5f956de7e4633d7dc92ea20c118e3 (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.erl93
-rw-r--r--src/dnssecport.erl92
-rw-r--r--src/v1.erl26
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]),
diff --git a/src/v1.erl b/src/v1.erl
index 72d0112..c1b07e6 100644
--- a/src/v1.erl
+++ b/src/v1.erl
@@ -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.