From e751a89645f58aae29551e251edd57177aa7f026 Mon Sep 17 00:00:00 2001
From: Linus Nordberg <linus@nordu.net>
Date: Thu, 7 Apr 2016 16:04:31 +0200
Subject: Canonicalise DS RR and refactor dns a bit.

Rename split_rrset/1 -> decode_rrset/1.
Add type rr() and use it.
Canonicalise DS RR.
---
 src/dns.erl        | 74 ++++++++++++++++++++++++++++++++++++++----------------
 src/dnssecport.erl | 27 ++++++++++++--------
 src/v1.erl         |  2 +-
 3 files changed, 70 insertions(+), 33 deletions(-)

(limited to 'src')

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
     }.
 
diff --git a/src/v1.erl b/src/v1.erl
index bab77aa..ef9aadd 100644
--- a/src/v1.erl
+++ b/src/v1.erl
@@ -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} ->
-- 
cgit v1.1