diff options
-rw-r--r-- | src/dns.erl | 74 | ||||
-rw-r--r-- | src/dnssecport.erl | 27 | ||||
-rw-r--r-- | src/v1.erl | 2 |
3 files changed, 70 insertions, 33 deletions
diff --git a/src/dns.erl b/src/dns.erl index 24fb8fb..b8a8ffe 100644 --- a/src/dns.erl +++ b/src/dns.erl @@ -2,22 +2,33 @@ %%% See LICENSE for licensing information. -module(dns). --export([split_rrset/1, encode_rr/1, encode_rrset/1]). +-export([decode_rrset/1, decode_rr/1, encode_rrset/1, encode_rr/1, + canonicalize_dsrr/2]). -decode_name_label(Name) -> - <<Len:8/integer, Label:Len/binary, Rest/binary>> = Name, - {Label, Rest}. +-record(rr, {name :: list(), % List of name labels. + type :: binary(), + class :: binary(), + ttl :: integer(), + rdata :: binary()}). +-type rr() :: #rr{}. +-spec decode_name_label(binary()) -> tuple(). +decode_name_label(RRbin) -> + <<Len:8/integer, Label:Len/binary, Rest/binary>> = RRbin, + {binary_to_list(Label), Rest}. + +-spec encode_name_label(string()) -> binary(). encode_name_label(Label) -> - Len = byte_size(Label), - <<Len:8/integer, Label/binary>>. + LabelBin = list_to_binary(Label), + Len = byte_size(LabelBin), + <<Len:8/integer, LabelBin/binary>>. -decode_name(RR) -> - decode_name(RR, []). +decode_name(RRbin) -> + decode_name(RRbin, []). decode_name(<<0, Rest/binary>>, Acc) -> {lists:reverse(Acc), Rest}; -decode_name(Name, Acc) -> - {Label, Rest} = decode_name_label(Name), +decode_name(RRbin, Acc) -> + {Label, Rest} = decode_name_label(RRbin), decode_name(Rest, [Label | Acc]). -spec encode_name(list()) -> binary(). @@ -29,28 +40,29 @@ encode_name([], Acc) -> encode_name([H|T], Acc) -> encode_name(T, [encode_name_label(H) | Acc]). --spec decode_rr(binary()) -> {list(), binary()}. -decode_rr(RR) -> - {Name, RestRR} = decode_name(RR), +-spec decode_rr(binary()) -> {rr(), binary()}. +decode_rr(RRBin) -> + {Name, RestRR} = decode_name(RRBin), <<Type:2/binary, Class:2/binary, TTL:4/integer-unit:8, RDLength:2/integer-unit:8, RDATA:RDLength/binary, Rest/binary>> = RestRR, - {[Name, Type, Class, TTL, RDATA], Rest}. + {#rr{name = Name, type = Type, class = Class, ttl = TTL, rdata = RDATA}, + Rest}. --spec split_rrset(binary()) -> list(). -split_rrset(RRSet) -> - split_rrset(RRSet, []). -split_rrset(<<>>, Acc) -> +-spec decode_rrset(binary()) -> [rr()]. +decode_rrset(RRSet) -> + decode_rrset(RRSet, []). +decode_rrset(<<>>, Acc) -> lists:reverse(Acc); -split_rrset(RRSet, Acc) -> +decode_rrset(RRSet, Acc) -> {RR, Rest} = decode_rr(RRSet), - split_rrset(Rest, [RR | Acc]). + decode_rrset(Rest, [RR | Acc]). --spec encode_rr(list()) -> binary(). -encode_rr([Name, Type, Class, TTL, RDATA]) -> +-spec encode_rr(rr()) -> binary(). +encode_rr(#rr{name = Name, type = Type, class = Class, ttl = TTL, rdata = RDATA}) -> EncodedName = encode_name(Name), RDLength = byte_size(RDATA), <<EncodedName/binary, @@ -67,3 +79,21 @@ encode_rrset([], Acc) -> list_to_binary(lists:reverse(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, + + DS#rr{name = LCName, ttl = OrigTTL}. + +%% TODO: Add unit tests. diff --git a/src/dnssecport.erl b/src/dnssecport.erl index 02f919a..c942fb4 100644 --- a/src/dnssecport.erl +++ b/src/dnssecport.erl @@ -30,7 +30,7 @@ init(Program) -> decode_response(Response) -> <<Status:16/integer, RRSet/binary>> = Response, - {ok, Status, dns:split_rrset(RRSet)}. + {ok, Status, dns:decode_rrset(RRSet)}. handle_call(stop, _From, State) -> lager:debug("dnssec stop request received"), @@ -45,13 +45,12 @@ handle_call({validate, Data}, _From, State) -> {Port, {data, Response}} -> case decode_response(list_to_binary(Response)) of {ok, 400, [DS | Chain]} -> - {reply, - {ok, [dns:encode_rr(DS) | dns:encode_rrset(Chain)]}, - State}; + RRSIG = hd(Chain), + R = [dns:encode_rr(dns:canonicalize_dsrr(DS, RRSIG)), + dns:encode_rrset(Chain)], + {reply, {ok, R}, State}; {ok, Error, _} -> - {reply, {error, Error}, State}; - {error, Reason} -> - {stop, {protocolerror, Reason}, State} + {reply, {error, Error}, State} end; {Port, {exit_status, ExitStatus}} -> lager:error("dnssec port ~p exiting with status ~p", @@ -103,6 +102,7 @@ stop_port(State) -> %% Unit tests. -define(TA_FILE, "test/testdata/dnssec/trust_anchors"). -define(REQ1_FILE, "test/testdata/dnssec/req.1"). +-define(REQ2_FILE, "test/testdata/dnssec/req-lowttl"). start_test_port() -> create_port("priv/dnssecport", [?TA_FILE]). @@ -113,13 +113,14 @@ stop_test_port(Port) -> read_submission_from_file(Filename) -> {ok, Data} = file:read_file(Filename), - dns:split_rrset(Data). + dns:decode_rrset(Data). read_dec_enc_test_() -> DecodedRRset = read_submission_from_file(?REQ1_FILE), {ok, FileContent} = file:read_file(?REQ1_FILE), [?_assertEqual(FileContent, dns:encode_rrset(DecodedRRset))]. +%% TODO: These tests are a bit lame. Room for improvement! full_test_() -> {setup, fun() -> @@ -127,10 +128,16 @@ full_test_() -> fun(Port) -> stop_test_port(Port) end, fun(Port) -> - R = handle_call({validate, read_submission_from_file(?REQ1_FILE)}, + R1 = handle_call({validate, read_submission_from_file(?REQ1_FILE)}, 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), [ - ?_assertMatch({reply, {ok, _}, _State}, R) + ?_assertMatch({reply, {ok, _}, _State}, R1), + ?_assertMatch({reply, {ok, _}, _State}, R2), + ?_assertMatch({rr, _Name, _Type, _Class, 3600, _RDATA}, DS) ] end }. @@ -174,7 +174,7 @@ decode_chain(List) -> end. add_ds_helper(Data) -> - case dnssecport:dnssec_validate(Data) of + case dnssecport:validate(Data) of {ok, [DS | Chain]} -> success(catlfish:add_chain(DS, Chain, normal)); {error, ErrorCode} -> |